From 8e80da70745422b837a298f7310d941eaa410aa1 Mon Sep 17 00:00:00 2001 From: Aaron S Date: Thu, 25 Aug 2022 16:07:43 -0500 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 511751a56b6bb567a5d8b874f4de79a13e21a773 Author: Jon Wire Date: Thu Aug 25 11:52:56 2022 -0500 chore: fixed build, minimatch dep (#10261) commit 4217e83016af40f65d322a3e4733f13f5ea29222 Author: aws-amplify-bot Date: Tue Aug 23 23:14:03 2022 +0000 chore(release): update version.ts [ci skip] commit da29d79af1d5bfa585588eac6ac20982891be9af Author: aws-amplify-bot Date: Tue Aug 23 23:11:11 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.60 - @aws-amplify/ui-components@1.9.31 - @aws-amplify/ui-react@1.2.51 - @aws-amplify/ui-storybook@2.0.51 - @aws-amplify/ui-vue@1.1.45 - @aws-amplify/analytics@5.2.18 - @aws-amplify/api-graphql@2.3.15 - @aws-amplify/api-rest@2.0.51 - @aws-amplify/api@4.0.51 - @aws-amplify/auth@4.6.4 - aws-amplify-angular@6.0.51 - aws-amplify-react@5.1.34 - aws-amplify@4.3.33 - @aws-amplify/cache@4.0.53 - @aws-amplify/core@4.7.2 - @aws-amplify/datastore-storage-adapter@1.3.11 - @aws-amplify/datastore@3.12.8 - @aws-amplify/geo@1.3.14 - @aws-amplify/interactions@4.0.51 - @aws-amplify/predictions@4.0.51 - @aws-amplify/pubsub@4.5.1 - @aws-amplify/pushnotification@4.3.30 - @aws-amplify/storage@4.5.4 - @aws-amplify/xr@3.0.51 commit 7d65e44794c9dd38808918569dccc4d6e25f7795 Author: Aaron S Date: Tue Aug 23 17:34:16 2022 -0500 chore: preparing release commit 01aad60ff14c3db47761db819dd47def75bfcb9d Author: Ashwin Kumar Date: Mon Aug 22 18:05:49 2022 -0700 fix(interactions): fix addPluggable API (#10250) * fix(interactions): fix addPluggable API * fix(interactions): remove Add a invalid pluggable test Co-authored-by: Sridhar commit 2b54c1ab3a83cc51778215f5e200669e25abdccc Author: aws-amplify-bot Date: Thu Aug 18 23:50:36 2022 +0000 chore(release): update version.ts [ci skip] commit 2e016a69956f2bae1e88c4bf6ffb3af628aed454 Author: aws-amplify-bot Date: Thu Aug 18 23:47:58 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.59 - @aws-amplify/ui-components@1.9.30 - @aws-amplify/ui-react@1.2.50 - @aws-amplify/ui-storybook@2.0.50 - @aws-amplify/ui-vue@1.1.44 - @aws-amplify/analytics@5.2.17 - @aws-amplify/api-graphql@2.3.14 - @aws-amplify/api-rest@2.0.50 - @aws-amplify/api@4.0.50 - @aws-amplify/auth@4.6.3 - aws-amplify-angular@6.0.50 - aws-amplify-react@5.1.33 - aws-amplify@4.3.32 - @aws-amplify/cache@4.0.52 - @aws-amplify/core@4.7.1 - @aws-amplify/datastore-storage-adapter@1.3.10 - @aws-amplify/datastore@3.12.7 - @aws-amplify/geo@1.3.13 - @aws-amplify/interactions@4.0.50 - @aws-amplify/predictions@4.0.50 - @aws-amplify/pubsub@4.5.0 - @aws-amplify/pushnotification@4.3.29 - @aws-amplify/storage@4.5.3 - @aws-amplify/xr@3.0.50 commit 543014d86e9dc38494a515a6c1435f9aae1c4a02 Author: Katie Goines Date: Thu Aug 18 16:08:51 2022 -0700 chore: preparing release commit d6cb7f95d68b05c9668bfd1f823b9db264f6291e Author: Aaron S <94858815+stocaaro@users.noreply.github.com> Date: Thu Aug 18 16:39:46 2022 -0500 fix(pubsub): Connection Ack verification bug (#10200) * fex(pubsub): Add distinct RN Reachibility implementation * fix(PubSub): Monitor tests * fix(pubsub): Connection Ack verification bug * Fix the test * fix: Add tests to cover the fixed bug * Update packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/index.ts Co-authored-by: Francisco Rodriguez * Update packages/pubsub/__tests__/AWSAppSyncRealTimeProvider.test.ts Co-authored-by: Francisco Rodriguez * Test fix * Fix test Co-authored-by: Francisco Rodriguez commit f28918b1ca1111f98c231c8ed6bccace9ad9e607 Author: Aaron S <94858815+stocaaro@users.noreply.github.com> Date: Wed Aug 17 15:55:05 2022 -0500 feat: PubSub Connection state tracking for MQTT and IoT providers (#10136) * feat: PubSub Connection state tracking for MQTT and IoT providers * fix: Add disconnect message on connection lost commit d4c395520bc66f24325babbe53e6ab7ebdea4d3b Author: Ashwin Kumar Date: Wed Aug 17 10:25:13 2022 -0700 fix(interactions): fix configure default provider (#10215) * fix(interactions): fix configure default provider * chore: pin @types/lodash version in aws-amplify-angular Co-authored-by: Sridhar commit f54b64502bb163307a1e8701d686d9b6b90a6eee Author: Aaron S <94858815+stocaaro@users.noreply.github.com> Date: Wed Aug 17 11:05:26 2022 -0500 fix: An update to @types/lodash breaks the build - specify last working version to unblock (#10221) commit 51a4598d3c697c492c1afa573b7b830540132b72 Author: Ashwin Kumar Date: Tue Aug 16 11:33:45 2022 -0700 fix:(@aws-amplify/interactions): refactor-lex-v1 (#10155) commit 0ce6b956fe7babbc43293ff6826c368fb0bd4618 Author: aws-amplify-bot Date: Tue Aug 16 00:14:15 2022 +0000 chore(release): update version.ts [ci skip] commit d885ec21b12bb69a7229edd1578ab369646b7e88 Author: aws-amplify-bot Date: Tue Aug 16 00:11:38 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.58 - @aws-amplify/ui-components@1.9.29 - @aws-amplify/ui-react@1.2.49 - @aws-amplify/ui-storybook@2.0.49 - @aws-amplify/ui-vue@1.1.43 - @aws-amplify/analytics@5.2.16 - @aws-amplify/api-graphql@2.3.13 - @aws-amplify/api-rest@2.0.49 - @aws-amplify/api@4.0.49 - @aws-amplify/auth@4.6.2 - aws-amplify-angular@6.0.49 - aws-amplify-react@5.1.32 - aws-amplify@4.3.31 - @aws-amplify/cache@4.0.51 - @aws-amplify/core@4.7.0 - @aws-amplify/datastore-storage-adapter@1.3.9 - @aws-amplify/datastore@3.12.6 - @aws-amplify/geo@1.3.12 - @aws-amplify/interactions@4.0.49 - @aws-amplify/predictions@4.0.49 - @aws-amplify/pubsub@4.4.10 - @aws-amplify/pushnotification@4.3.28 - @aws-amplify/storage@4.5.2 - @aws-amplify/xr@3.0.49 commit 80cf2c856722cebb5509d2801448f14641b23fd1 Author: elorzafe Date: Mon Aug 15 14:49:48 2022 -0700 chore: preparing release commit f6e61b8fbe2e86497746cb2a473beba1bc312c97 Author: elorzafe Date: Mon Aug 15 14:44:29 2022 -0700 Revert "ci: automate GitHub releases with lerna (#10189)" This reverts commit f59fa9fb6b1243a02a59819da9389c428277130f. commit 74383d710607a27ffe843afe548eb990b3c32feb Author: Francisco Rodriguez Date: Thu Aug 11 14:11:06 2022 -0700 chore: ci: Disabling integ_rn_ios_datastore_sqlite_adapter test (#10193) * Disabling integ_rn_ios_datastore_sqlite_adapter test * Update config.yml commit f59fa9fb6b1243a02a59819da9389c428277130f Author: Ashwin Kumar Date: Thu Aug 11 09:05:50 2022 -0700 ci: automate GitHub releases with lerna (#10189) Co-authored-by: Sridhar Co-authored-by: Francisco Rodriguez commit 92cef8adee275bfbb4840be4cf49a7367fd95ce4 Author: Ashika <35131273+ashika01@users.noreply.github.com> Date: Tue Aug 9 16:55:55 2022 -0700 Fix: Analytics Type issue (#10185) * kinesis fix commit 5f427f3ae47d76231a81084c216527b7fabd668a Author: Aaron S <94858815+stocaaro@users.noreply.github.com> Date: Tue Aug 9 17:45:34 2022 -0500 fix(pubsub): Add distinct RN Reachibility implementation (#10175) * fix(pubsub): Add distinct RN Reachibility implementation * fix(PubSub): Fix monitor tests commit 88f118e1340c38ba237362644035b4d7c9f72557 Author: Ashika Kasiviswanathan Arumugakarthik Date: Tue Aug 9 15:04:13 2022 -0700 Revert "kinesis fix" This reverts commit 763609b4e92e75d5b1b4e17f7d9709aee8bcb510. commit 4e0e22bd733aac715681953dac90440a44fd49bd Author: Ashika Kasiviswanathan Arumugakarthik Date: Tue Aug 9 15:03:59 2022 -0700 Revert "update personalize type to accomodate string" This reverts commit 9326bebcde32bda54a487a6bcd8e37f8c0d7e110. commit 9326bebcde32bda54a487a6bcd8e37f8c0d7e110 Author: Ashika Kasiviswanathan Arumugakarthik Date: Tue Aug 9 14:39:19 2022 -0700 update personalize type to accomodate string commit 763609b4e92e75d5b1b4e17f7d9709aee8bcb510 Author: Ashika Kasiviswanathan Arumugakarthik Date: Tue Aug 9 12:27:47 2022 -0700 kinesis fix commit 850788ca08974eca26eefdd595fcab31939b6808 Author: Katie Goines <30757403+katiegoines@users.noreply.github.com> Date: Mon Aug 8 17:15:40 2022 -0700 Updating config.yml to mitigate circle CI pipeline failures from outdated Xcode image (#10158) * updating config.yml for testing changes to staging * removing android integ tests for now * Update config.yml * removing code used for testing Co-authored-by: Francisco Rodriguez commit 88a9ec97fca2eb19c9cc9496b8b7d25b75f02073 Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Fri Aug 5 16:31:56 2022 -0400 fix(datastore): make di context fields private (#10162) commit 360bde20716778b69af339f4f66b42c05ccf4639 Author: Amelia Hill <49414147+amehi0index@users.noreply.github.com> Date: Thu Aug 4 11:49:38 2022 -0700 feat(@aws-amplify/core): Throw Error if body attribute passed to Sign… (#10137) * feat(@aws-amplify/core): Throw Error if body attribute passed to Signer.sign() Co-authored-by: Ahilash Sasidharan ahilashs@yahoo.com * Set space-before-function-paren to false for unit test * Make changes in response to PR comments * Make changes in response to PR comments * Find issue with unit tests * Move sign error tests into Signer-test * Set space-before-function-paren to false for unit test * Run test with space-before-function-paren set to true * Fix space before function error * Update tslint.json Set "space-before-function-paren" back to default setting. * Remove spaces before keyword "function" Return original formatting rule of no-spaces before "function" keyword. Co-authored-by: Ashika <35131273+ashika01@users.noreply.github.com> commit dbe57a4ae45fdac9f8a5990065495b0d04639267 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Aug 3 15:14:51 2022 -0400 Revert "Merge branch 'expo-sqlite-adapter-unit-tests' into main" (#10151) This reverts commit d8637cc8c68e1214d29f8044be78322d3ce9f2ae, reversing changes made to 186349e1ec885f1e73ba24f71c3cd7ab4d88fe8a. commit d8637cc8c68e1214d29f8044be78322d3ce9f2ae Merge: 186349e1e a22b96212 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Aug 3 14:01:36 2022 -0400 Merge branch 'expo-sqlite-adapter-unit-tests' into main commit 186349e1ec885f1e73ba24f71c3cd7ab4d88fe8a Author: aws-amplify-bot Date: Mon Aug 1 22:10:37 2022 +0000 chore(release): update version.ts [ci skip] commit 7c46fbc75cff43c6323d29384f01cee7d3483e40 Author: aws-amplify-bot Date: Mon Aug 1 22:08:14 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.57 - @aws-amplify/ui-components@1.9.28 - @aws-amplify/ui-react@1.2.48 - @aws-amplify/ui-storybook@2.0.48 - @aws-amplify/ui-vue@1.1.42 - @aws-amplify/analytics@5.2.15 - @aws-amplify/api-graphql@2.3.12 - @aws-amplify/api-rest@2.0.48 - @aws-amplify/api@4.0.48 - @aws-amplify/auth@4.6.1 - aws-amplify-angular@6.0.48 - aws-amplify-react@5.1.31 - aws-amplify@4.3.30 - @aws-amplify/cache@4.0.50 - @aws-amplify/core@4.6.1 - @aws-amplify/datastore-storage-adapter@1.3.8 - @aws-amplify/datastore@3.12.5 - @aws-amplify/geo@1.3.11 - @aws-amplify/interactions@4.0.48 - @aws-amplify/predictions@4.0.48 - @aws-amplify/pubsub@4.4.9 - @aws-amplify/pushnotification@4.3.27 - @aws-amplify/storage@4.5.1 - @aws-amplify/xr@3.0.48 commit 53a38db6ed35a91e0c2b90a5b2a0fadc59cef20d Author: elorzafe Date: Mon Aug 1 14:12:11 2022 -0700 chore: preparing release commit 06504e649068f01b85392373fdf80e2ed2a6cada Author: Olya Balashova <42189299+helgabalashova@users.noreply.github.com> Date: Mon Aug 1 13:00:52 2022 -0600 fix(@aws-amplify/auth): fix storage bug for auto sign in value (#10139) Co-authored-by: Balashova Co-authored-by: Francisco Rodriguez commit eb7a2c49b4d2cefa5c611f331b0d0ebe397cd88a Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat Jul 30 11:50:11 2022 -0400 chore(deps): bump tzinfo from 1.2.9 to 1.2.10 in /docs (#10102) commit d5cb22386653916ff842153c88080f92c6f604b5 Author: Aaron S <94858815+stocaaro@users.noreply.github.com> Date: Fri Jul 29 13:43:02 2022 -0500 GH-9824 - PubSub Connection state tracking for AppSyncRealtime (#10063) Co-authored-by: Francisco Rodriguez commit 2ac903516ec295fbf098f6a6644000177f315184 Author: Kha Truong <64438356+khatruong2009@users.noreply.github.com> Date: Fri Jul 29 11:39:07 2022 -0400 fix(auth): Unauthenticated identity throws AuthError without user … (#10090) Co-authored-by: Aaron S <94858815+stocaaro@users.noreply.github.com> commit e1979509ac4c1e3bea3b3f4ce9b45efc0727a1de Author: Amelia Hill <49414147+amehi0index@users.noreply.github.com> Date: Fri Jul 29 08:19:11 2022 -0700 Fix grammar errors and clarify testing requirements (#10122) Co-authored-by: Aaron S <94858815+stocaaro@users.noreply.github.com> commit fa291b39dc84231ccaee11b3c3b6a2cb009bdc2d Author: aws-amplify-bot Date: Thu Jul 28 23:08:23 2022 +0000 chore(release): update version.ts [ci skip] commit a994818501fed755b7ba226f6e20cb1981a2efd4 Author: aws-amplify-bot Date: Thu Jul 28 23:05:52 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.56 - @aws-amplify/ui-components@1.9.27 - @aws-amplify/ui-react@1.2.47 - @aws-amplify/ui-storybook@2.0.47 - @aws-amplify/ui-vue@1.1.41 - @aws-amplify/analytics@5.2.14 - @aws-amplify/api-graphql@2.3.11 - @aws-amplify/api-rest@2.0.47 - @aws-amplify/api@4.0.47 - @aws-amplify/auth@4.6.0 - aws-amplify-angular@6.0.47 - aws-amplify-react@5.1.30 - aws-amplify@4.3.29 - @aws-amplify/cache@4.0.49 - @aws-amplify/core@4.6.0 - @aws-amplify/datastore-storage-adapter@1.3.7 - @aws-amplify/datastore@3.12.4 - @aws-amplify/geo@1.3.10 - @aws-amplify/interactions@4.0.47 - @aws-amplify/predictions@4.0.47 - @aws-amplify/pubsub@4.4.8 - @aws-amplify/pushnotification@4.3.26 - @aws-amplify/storage@4.5.0 - @aws-amplify/xr@3.0.47 commit 60c128ac547b17299036aa1787ced8af7a88a40c Author: James Au Date: Thu Jul 28 15:15:20 2022 -0700 chore: preparing release commit e54617f2878244f0e391d2d49f5cd2e8a8c069f9 Author: Olya Balashova <42189299+helgabalashova@users.noreply.github.com> Date: Thu Jul 28 13:50:58 2022 -0600 feat(@aws-amplify/auth): Auto sign in after sign up (#10126) Fixes: #6320 #3882 #3631 #6018 Co-authored-by: Balashova Co-authored-by: Francisco Rodriguez commit 366c32e2d87d73210bbd01ca1da55a5899f5a503 Author: Venkata Ramyasri Kota <34170013+kvramyasri7@users.noreply.github.com> Date: Thu Jul 28 07:23:33 2022 -0700 feat(@aws-amplify/storage): Access all files from S3 with List API (#10095) Co-authored-by: Aaron S <94858815+stocaaro@users.noreply.github.com> commit 0729e683a01536e583e707600be76077d8e16050 Author: Amelia Hill <49414147+amehi0index@users.noreply.github.com> Date: Wed Jul 27 09:41:11 2022 -0700 Fix typo in contributing guide (#10104) commit 63fcc695767cb23ea7f689d68a55b28e8f9bdcf0 Author: aws-amplify-bot Date: Thu Jul 21 23:19:11 2022 +0000 chore(release): update version.ts [ci skip] commit e20a1468ebed5b62512e7bdbfc2d2c185a60f9c7 Author: aws-amplify-bot Date: Thu Jul 21 23:16:41 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.55 - @aws-amplify/ui-components@1.9.26 - @aws-amplify/ui-react@1.2.46 - @aws-amplify/ui-storybook@2.0.46 - @aws-amplify/ui-vue@1.1.40 - @aws-amplify/analytics@5.2.13 - @aws-amplify/api-graphql@2.3.10 - @aws-amplify/api-rest@2.0.46 - @aws-amplify/api@4.0.46 - @aws-amplify/auth@4.5.10 - aws-amplify-angular@6.0.46 - aws-amplify-react@5.1.29 - aws-amplify@4.3.28 - @aws-amplify/cache@4.0.48 - @aws-amplify/core@4.5.10 - @aws-amplify/datastore-storage-adapter@1.3.6 - @aws-amplify/datastore@3.12.3 - @aws-amplify/geo@1.3.9 - @aws-amplify/interactions@4.0.46 - @aws-amplify/predictions@4.0.46 - @aws-amplify/pubsub@4.4.7 - @aws-amplify/pushnotification@4.3.25 - @aws-amplify/storage@4.4.29 - @aws-amplify/xr@3.0.46 commit 9080bed3aa66564444be56f0c3666d64cc73534d Author: elorzafe Date: Thu Jul 21 15:27:34 2022 -0700 chore: preparing release commit b7ad1260eb1bc6611cd902e7b8b3e58066ef22b4 Author: James Au <40404256+jamesaucode@users.noreply.github.com> Date: Tue Jul 19 19:03:02 2022 -0700 fix: Update AmazonPersonalizeProvider Analytics typings (#10076) commit a10d920f7fb6199539fb8d9cec2cb4426dbfd47b Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Tue Jul 19 15:30:40 2022 -0400 fix: preserve ssr context when using DataStore (#10088) commit dfe6461aab6e26cdb259358b9399c48f03c44a8e Author: David McAfee Date: Mon Jul 18 18:07:36 2022 -0400 Update .github/CODEOWNERS Co-authored-by: Jon Wire commit da4a92789ebf72866c64c7de6299412508a64102 Author: David McAfee Date: Mon Jul 18 17:15:42 2022 -0400 chore(data): update CODEOWNERS file commit 633a83923cb5c6111e9fed7b27fe210ec0c13f20 Author: erinleigh90 <106691284+erinleigh90@users.noreply.github.com> Date: Mon Jul 18 15:18:50 2022 -0700 Datastore/feat user agent suffix (#10086) * Adds suffix to user agent for calls to API initiated by DataStore * Attempts to fix first half of user agent not being sent * Makes setting of user agent header more concise * Moves appending of suffix to user agent to core library * Moves user agent suffix constant to common util in datastore * Removes unused import * Unit test for api-graphql * Pulls in user agent suffix from datastore utils class * Adds unit test for getAmplifyUserAgent with and without content * Fixes issue found while testing, line too long. * Adds test for DataStore mutation.ts * Tests user agent suffix in datastore sync * Adds user agent suffix assertion to subscription test * Fixes variable declaration: const instead of let * Removes leftover lines of code from testing objectContains * Removes code style changes unrelated to user agent suffix * Removes code style changes unrelated to user agent suffix * Removes unnecessary null value option for userAgentSuffix - undefined is sufficient * Replaces imports from lib-esm * Replaces hard-coded string in assertion with constant from util file * Moves var declaration under import * Makes test method names more descriptive Co-authored-by: Erin Beal commit 8e49b0d53bc7a8c373289cf5aa2172284c76ecd6 Author: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed Jul 13 10:28:29 2022 -0400 chore(datastore): Add schema-drift integration test (#10077) commit 099270437e871c3d1801e8a66d5e0dae93f930c9 Author: Dane Pilcher Date: Mon Jul 11 14:00:13 2022 -0600 chore: add dpilch to datastore codeowners (#10031) commit f6e2ca25785bb30ab6040f1f8c163e6069ccc392 Author: aws-amplify-bot Date: Thu Jul 7 22:44:30 2022 +0000 chore(release): update version.ts [ci skip] commit d3c993fc2bf9505a05837b889f4727589822f0bd Author: aws-amplify-bot Date: Thu Jul 7 22:42:02 2022 +0000 chore(release): Publish [ci skip] - amazon-cognito-identity-js@5.2.10 - @aws-amplify/ui-angular@1.0.54 - @aws-amplify/ui-components@1.9.25 - @aws-amplify/ui-react@1.2.45 - @aws-amplify/ui-storybook@2.0.45 - @aws-amplify/ui-vue@1.1.39 - @aws-amplify/analytics@5.2.12 - @aws-amplify/api-graphql@2.3.9 - @aws-amplify/api-rest@2.0.45 - @aws-amplify/api@4.0.45 - @aws-amplify/auth@4.5.9 - aws-amplify-angular@6.0.45 - aws-amplify-react@5.1.28 - aws-amplify-vue@2.1.7 - aws-amplify@4.3.27 - @aws-amplify/cache@4.0.47 - @aws-amplify/core@4.5.9 - @aws-amplify/datastore-storage-adapter@1.3.5 - @aws-amplify/datastore@3.12.2 - @aws-amplify/geo@1.3.8 - @aws-amplify/interactions@4.0.45 - @aws-amplify/predictions@4.0.45 - @aws-amplify/pubsub@4.4.6 - @aws-amplify/pushnotification@4.3.24 - @aws-amplify/storage@4.4.28 - @aws-amplify/xr@3.0.45 commit 704dfb371cc959ee4d647f4cd3ccaa9e849d4a22 Author: Aaron S Date: Thu Jul 7 16:34:05 2022 -0500 chore: preparing release commit de0441b4fa67409ccbc630c42890e2c58ee779fb Author: Satana Charuwichitratana Date: Wed Jul 6 07:22:29 2022 +0700 fix(amazon-cognito-identity-js): Missing cognito user challenge name … (#10047) * fix(amazon-cognito-identity-js): Missing cognito user challenge name type * fix(type): Fix other functions reported in #6974 * Update Auth units with correct ChallengeName Co-authored-by: elorzafe commit 870ec87a6b5d6f3aa3a85551faac87a208c354a2 Author: James Au <40404256+jamesaucode@users.noreply.github.com> Date: Fri Jul 1 15:18:35 2022 -0700 fix: pin vue version (#10052) commit b454b5ccde0d195998bd1dfd1f8b457935f1982d Author: Caleb Pollman Date: Thu Jun 30 14:23:55 2022 -0700 chore(CODEOWNERS): update geo category codeowners (#10048) commit 3dd903573843f6f53251d118a11de7dacd9edddd Author: Chris F <5827964+cshfang@users.noreply.github.com> Date: Wed Jun 29 12:25:15 2022 -0700 fix(analytics): Buffer limit should be adhered to (#10015) Co-authored-by: Chris Fang commit 11b537c62fee74c04e4e3b72ba43a353ba5152c9 Author: James Au <40404256+jamesaucode@users.noreply.github.com> Date: Wed Jun 29 11:31:34 2022 -0700 fix: Update Auth to import JS using named export (#10033) Imports specific functions instead of the whole JS module to improve bundle size commit fb1f02cfa914b81fe0411e8f4d654c69aed22385 Author: Dane Pilcher Date: Tue Jun 28 15:18:18 2022 -0600 fix: decrease error handler verbosity on self recovering errors (#10030) restore warning message commit 9cca114895d931365be4d22f52adb60232a3d242 Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Thu Jun 23 16:08:52 2022 -0400 ci: canaries rn workaround (#10020) commit 5e8264963c19b50069e9acc2b692d8e6a1a0a201 Author: aws-amplify-bot Date: Sat Jun 18 02:53:49 2022 +0000 chore(release): update version.ts [ci skip] commit f206bf675cd63bee4147984b050036c82905bf27 Author: aws-amplify-bot Date: Sat Jun 18 02:51:33 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.53 - @aws-amplify/ui-components@1.9.24 - @aws-amplify/ui-react@1.2.44 - @aws-amplify/ui-storybook@2.0.44 - @aws-amplify/ui-vue@1.1.38 - @aws-amplify/analytics@5.2.11 - @aws-amplify/api-graphql@2.3.8 - @aws-amplify/api-rest@2.0.44 - @aws-amplify/api@4.0.44 - @aws-amplify/auth@4.5.8 - aws-amplify-angular@6.0.44 - aws-amplify-react@5.1.27 - aws-amplify@4.3.26 - @aws-amplify/cache@4.0.46 - @aws-amplify/core@4.5.8 - @aws-amplify/datastore-storage-adapter@1.3.4 - @aws-amplify/datastore@3.12.1 - @aws-amplify/geo@1.3.7 - @aws-amplify/interactions@4.0.44 - @aws-amplify/predictions@4.0.44 - @aws-amplify/pubsub@4.4.5 - @aws-amplify/pushnotification@4.3.23 - @aws-amplify/storage@4.4.27 - @aws-amplify/xr@3.0.44 commit b339298ab7457941fed8ba4a40de4c2cda8eb5a4 Author: David McAfee Date: Fri Jun 17 19:20:23 2022 -0700 chore: preparing release commit eb73ad70b3eee0632eaed4bae00f1d2179ae45b5 Author: David McAfee Date: Fri Jun 17 18:32:11 2022 -0700 Revert "fix: decrease error handler verbosity on self recovering errors (#9987)" (#10004) This reverts commit 67ccf09a93221a06d4560300cfd67fdd9efeda71. commit 8fb868faeb118854b51afcd297a7762cbadad628 Author: David McAfee Date: Fri Jun 17 17:16:28 2022 -0700 fix: update geo integ tests to use chrome due to issue with CRA Jest dependency when using Node 16.5.x (#10003) commit 8b6728ddc0f2eba10f33b3e8f891acadc1a7f66d Author: Jon Wire Date: Fri Jun 17 16:18:41 2022 -0500 inc timeouts to stabilize observequery unit tests which are flakey in circleci commit b5c6825a28e58986b26cce662f8db7a3623146e7 Author: David McAfee Date: Fri Jun 17 12:49:55 2022 -0700 fix: remove comments commit 67316d78fd829b9d4875a25d00719b175738e594 Author: David McAfee Date: Thu Jun 16 15:29:29 2022 -0700 fix: update axios commit 67ccf09a93221a06d4560300cfd67fdd9efeda71 Author: Dane Pilcher Date: Fri Jun 17 11:05:50 2022 -0600 fix: decrease error handler verbosity on self recovering errors (#9987) * fix: decrease error handler verbosity on self recovering errors * style: remove unused vars * test: increase test coverage on auth mode retry error handler Co-authored-by: Jon Wire commit e6e7b1362fca733aa8e594afdbf52ad3f0253858 Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Thu Jun 16 13:08:40 2022 -0400 ci: canaries - improve retry command (#9996) commit 0fb173de9abd6214b9957ef87a5a9d0f9e60f3b5 Author: aws-amplify-bot Date: Wed Jun 15 22:50:19 2022 +0000 chore(release): update version.ts [ci skip] commit d4364447760f62f8aaa20c415747e9df753dbaea Author: aws-amplify-bot Date: Wed Jun 15 22:47:34 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.52 - @aws-amplify/ui-components@1.9.23 - @aws-amplify/ui-react@1.2.43 - @aws-amplify/ui-storybook@2.0.43 - @aws-amplify/ui-vue@1.1.37 - @aws-amplify/analytics@5.2.10 - @aws-amplify/api-graphql@2.3.7 - @aws-amplify/api-rest@2.0.43 - @aws-amplify/api@4.0.43 - @aws-amplify/auth@4.5.7 - aws-amplify-angular@6.0.43 - aws-amplify-react-native@6.0.5 - aws-amplify-react@5.1.26 - aws-amplify@4.3.25 - @aws-amplify/cache@4.0.45 - @aws-amplify/core@4.5.7 - @aws-amplify/datastore-storage-adapter@1.3.3 - @aws-amplify/datastore@3.12.0 - @aws-amplify/geo@1.3.6 - @aws-amplify/interactions@4.0.43 - @aws-amplify/predictions@4.0.43 - @aws-amplify/pubsub@4.4.4 - @aws-amplify/pushnotification@4.3.22 - @aws-amplify/storage@4.4.26 - @aws-amplify/xr@3.0.43 commit 97f98fcae5745f3f92f823a550874568261dbe53 Author: David McAfee Date: Wed Jun 15 14:44:13 2022 -0700 chore: preparing release commit 88b6a1e82445c359c930ae40a9028ab250870d74 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Tue Jun 14 15:32:34 2022 -0400 fix: Add module declaration files for datastore-storage-adapter (#9922) * Add module declaration files * Update the module declaration to index files Co-authored-by: Nick Arocho <16296496+nickarocho@users.noreply.github.com> commit ca2a11b5bc987e71ce3344058a4886bf067cb17b Author: Jon Wire Date: Mon Jun 13 16:00:11 2022 -0500 fix(@aws-amplify/datastore): adds missing fields to items sent through observe/observeQuery (#9973) * manual rebase: add missing fields to items sent through observe/observequery * added testing to ensure outbox only contains expected fields * removed cruft; removed explicit check for undef mutation field commit d5dd9cb5bf46131fb046cfe55e4899444f9d789e Author: Dane Pilcher Date: Mon Jun 13 11:03:21 2022 -0600 fix: merge patches for consecutive copyOf (#9936) * test: add unit test for consecutive copyOf * fix: merge patches for consecutive copyOf commit bcb7fa67a6ba5662989465f6c96ead1706ee1f40 Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Fri Jun 10 16:00:15 2022 -0400 ci: revert canaries change (#9980) commit e9cb92cd9ee0bc0fd66b2fbbeabbbc7f501f31c2 Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Fri Jun 10 13:21:22 2022 -0400 ci: correct run-with-retry command (#9978) commit d1356b1e498eb71a4902892afbb41f6ff88abb6f Author: Jon Wire Date: Thu Jun 9 15:37:29 2022 -0500 fix(@aws-amplify/datastore): fixes observeQuery not removing newly-filtered items from snapshot (#9879) * test: added observeQuery tests; one skipped, intended to show correct behavior ahead of fix * working fix; still needs cleanup * a little cleanup * comment, docstring updates * more docstrings * fixed formatting in docstring * replaced naughty test pollution solution with better one * added test cases for delete commit 3a270969b6e097eeed73368091ace191cbc05511 Author: Dane Pilcher Date: Thu Jun 9 11:59:06 2022 -0600 feat(datastore): add error maps for error handler (#9918) * feat(datastore): add error map for newly required field * feat(datastore): add error map for unauthorized create * feat(datastore): add connection timeout error map * feat(datastore): add server error map * docs: add comment on error map util * test: add error map unit tests commit 61d60c7a2bf8123b6824e4473188bfcaa85c709c Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Wed Jun 8 18:28:26 2022 -0400 ci: restore xcode version for macos executor (#9974) commit 08b6c0c1b997d75338693089355805558c243f78 Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Wed Jun 8 16:38:44 2022 -0400 ci: add canaries workflow (#9953) commit bc9b710f2f0c493fd0cfea4fc0cb68573bc83072 Author: Nick Arocho <16296496+nickarocho@users.noreply.github.com> Date: Wed Jun 8 12:34:40 2022 -0700 chore(deps): bump dexie from 3.2.0 to 3.2.2 in DataStore (#9960) commit 1a5018a81471c3eb121c836e90d945efc4326fa5 Author: Shane Laymance Date: Tue Jun 7 10:02:04 2022 -0700 Add unit tests for validateGeofenceId (#9964) commit e7220da6712d352a46ed4f511407649e35e3faa1 Author: Jon Wire Date: Mon Jun 6 12:58:02 2022 -0500 resolve error-stack-parser, which introduced a build-blocked bug after 2.0.6 (#9963) commit e52e927a8d3e9e04b26c12c87a4d49d8123ba5cf Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Jun 3 12:14:45 2022 -0700 chore(deps): bump nokogiri from 1.13.4 to 1.13.6 in /docs (#9916) Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.4 to 1.13.6. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.4...v1.13.6) --- updated-dependencies: - dependency-name: nokogiri dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Wire Co-authored-by: Nick Arocho <16296496+nickarocho@users.noreply.github.com> commit 94813a9b364f9d13c72da38c0c13aefbe52157d7 Author: Ben Sewell Date: Wed Jun 1 16:14:30 2022 +0100 fix(aws-amplify-react-native): set Resend Code enabled/disabled from current username value (#9767) commit 9e3792f60a6bcc45588d17a74713832313b2ecdc Author: aws-amplify-bot Date: Tue May 24 02:40:49 2022 +0000 chore(release): update version.ts [ci skip] commit 78a6b547b69cc9032fd5ffce86285f38f2536b7f Author: aws-amplify-bot Date: Tue May 24 02:38:08 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.51 - @aws-amplify/ui-components@1.9.22 - @aws-amplify/ui-react@1.2.42 - @aws-amplify/ui-storybook@2.0.42 - @aws-amplify/ui-vue@1.1.36 - @aws-amplify/analytics@5.2.9 - @aws-amplify/api-graphql@2.3.6 - @aws-amplify/api-rest@2.0.42 - @aws-amplify/api@4.0.42 - @aws-amplify/auth@4.5.6 - aws-amplify-angular@6.0.42 - aws-amplify-react@5.1.25 - aws-amplify@4.3.24 - @aws-amplify/cache@4.0.44 - @aws-amplify/core@4.5.6 - @aws-amplify/datastore-storage-adapter@1.3.2 - @aws-amplify/datastore@3.11.3 - @aws-amplify/geo@1.3.5 - @aws-amplify/interactions@4.0.42 - @aws-amplify/predictions@4.0.42 - @aws-amplify/pubsub@4.4.3 - @aws-amplify/pushnotification@4.3.21 - @aws-amplify/storage@4.4.25 - @aws-amplify/xr@3.0.42 commit 3490c1a39550865fcccf97f9672a63de0ad8d8c9 Author: Jon Wire Date: Mon May 23 20:52:00 2022 -0500 chore: preparing release commit 40019a65b20527be68a95c65d3a84ddf2613e593 Author: Jon Wire Date: Mon May 23 20:48:10 2022 -0500 removed integ_duplicate_packages test dep commit cdc385d1740c945c9651a9a6edf3b9b9a47509b4 Author: Jon Wire Date: Mon May 23 20:18:58 2022 -0500 chore: preparing release commit 5f1eb2a4ec7356b160f262873d2348c37935bc22 Author: Jon Wire Date: Mon May 23 20:18:21 2022 -0500 updating analytics copyright date commit e5c0431dc15a22476d8b515531b55e18c5275be6 Author: Jon Wire Date: Mon May 23 20:06:58 2022 -0500 chore: preparing release commit b796a35863c9a335d25af14a2945484eeb421dc3 Author: aws-amplify-bot Date: Mon May 23 21:59:44 2022 +0000 chore(release): update version.ts [ci skip] commit e5858bc6c289de38a150e009d86ee43058545a32 Author: aws-amplify-bot Date: Mon May 23 21:57:08 2022 +0000 chore(release): Publish [ci skip] - amazon-cognito-identity-js@5.2.9 - @aws-amplify/ui-angular@1.0.50 - @aws-amplify/ui-components@1.9.21 - @aws-amplify/ui-react@1.2.41 - @aws-amplify/ui-storybook@2.0.41 - @aws-amplify/ui-vue@1.1.35 - @aws-amplify/analytics@5.2.8 - @aws-amplify/api-graphql@2.3.5 - @aws-amplify/api-rest@2.0.41 - @aws-amplify/api@4.0.41 - @aws-amplify/auth@4.5.5 - aws-amplify-angular@6.0.41 - aws-amplify-react@5.1.24 - aws-amplify@4.3.23 - @aws-amplify/cache@4.0.43 - @aws-amplify/core@4.5.5 - @aws-amplify/datastore-storage-adapter@1.3.1 - @aws-amplify/datastore@3.11.2 - @aws-amplify/geo@1.3.4 - @aws-amplify/interactions@4.0.41 - @aws-amplify/predictions@4.0.41 - @aws-amplify/pubsub@4.4.2 - @aws-amplify/pushnotification@4.3.20 - @aws-amplify/storage@4.4.24 - @aws-amplify/xr@3.0.41 commit 1b794b84040cf636d99b7dc1e403a651147f035d Author: Jon Wire Date: Mon May 23 16:19:47 2022 -0500 chore: preparing release commit 00923cfaeafcee97a0f54cc6aa04724f7155e75d Author: Jon Wire Date: Fri May 20 15:55:34 2022 -0500 fix(@aws-amplify/datastore-storage-adapter): remove extra, invalid sqlite mutations again (#9921) * testing expanded, refixed sqlite adapter * ported sqlite adapter test expansion to indexeddbadapter tests * working on making all adapter tests share common test script * shared tests working in both datastore and datastore-storage-adapter * moved and incorporated record adder helper in common adapter tests * cleanup cruft * cleaned up crufty lib include commit 7656bc8a9864af05618d3fd794a48217e9ec7295 Author: Dane Pilcher Date: Tue May 17 11:53:57 2022 -0600 refactor(datastore): add stub error maps (#9878) commit 08e01b1c09cfab73f2eb1b1b18fe1a696e2a028f Author: Luis Carlos <63477093+luis737@users.noreply.github.com> Date: Mon May 16 18:24:22 2022 -0100 fix: docs for amazon-cognito-identity-js (#9909) Co-authored-by: Ashika <35131273+ashika01@users.noreply.github.com> commit 798a8f075021582cc3e1f4f8ad239562ec4de566 Author: Katie Goines <30757403+katiegoines@users.noreply.github.com> Date: Mon May 16 11:10:21 2022 -0700 fix(@aws-amplify/storage): throw error if all upload parts complete but upload cannot be finished (#9317) * fix(@aws-amplify/storage): throw error if all upload parts complete but upload cannot be finished * fix(@aws-amplify/storage): updating tests * fix(@aws-amplify/storage): fixed unit tests * fix(@aws-amplify/storage): revert prettification * fix(@aws-amplify/storage): revert prettification * fix(@aws-amplify/storage): revert prettification * working on tests * response to auchu@ feedback * quick test fix * fix(storage): reverted trial changes * fix(storage): throw error if all upload parts complete but upload cannot be finished * fix(storage): resolving merge conflicts from main * fix(storage): adjusting tests * fix: Remove cancel variable entirely Co-authored-by: Aaron S <94858815+stocaaro@users.noreply.github.com> commit 29a40cc7488ced536ccc01a3ac6c0789fbe9b03d Author: aws-amplify-bot Date: Thu May 12 22:09:50 2022 +0000 chore(release): update version.ts [ci skip] commit 926e55adb976de85ed1ff9b55de7e1c6c97b3052 Author: aws-amplify-bot Date: Thu May 12 22:07:32 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.49 - @aws-amplify/ui-components@1.9.20 - @aws-amplify/ui-react@1.2.40 - @aws-amplify/ui-storybook@2.0.40 - @aws-amplify/ui-vue@1.1.34 - @aws-amplify/analytics@5.2.7 - @aws-amplify/api-graphql@2.3.4 - @aws-amplify/api-rest@2.0.40 - @aws-amplify/api@4.0.40 - @aws-amplify/auth@4.5.4 - aws-amplify-angular@6.0.40 - aws-amplify-react@5.1.23 - aws-amplify@4.3.22 - @aws-amplify/cache@4.0.42 - @aws-amplify/core@4.5.4 - @aws-amplify/datastore-storage-adapter@1.3.0 - @aws-amplify/datastore@3.11.1 - @aws-amplify/geo@1.3.3 - @aws-amplify/interactions@4.0.40 - @aws-amplify/predictions@4.0.40 - @aws-amplify/pubsub@4.4.1 - @aws-amplify/pushnotification@4.3.19 - @aws-amplify/storage@4.4.23 - @aws-amplify/xr@3.0.40 commit 98706a7e51de31b6d6b0c5523aac3b6e2b41020a Author: Katie Goines Date: Thu May 12 14:20:46 2022 -0700 chore: preparing release commit a63f0eec70b96dba2d220f3eeb0c799af8622b5c Author: Dane Pilcher Date: Thu May 12 08:59:30 2022 -0600 fix: add error for when schema is not initialized (#9874) * fix: add error for when schema is not initialized * chore: change warning to error * refactor: remove schemaInitialized variable commit a9ae27f65e1a782321c0be87556f92d2ee432352 Author: James Au <40404256+jamesaucode@users.noreply.github.com> Date: Tue May 10 10:37:51 2022 -0700 fix(@aws-amplify/api): graphql API.cancel fix (#9578) commit f72e3df196de37d2471a8b7d2eec9705bebd0b8b Author: Jon Wire Date: Fri May 6 14:40:57 2022 -0500 chore: remove arkam from codeowners (#9880) commit a8ed3c2fad0c780c8782e1729414afd51ff6b155 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Fri May 6 13:38:22 2022 -0400 feat: Added ExpoSQLiteAdapter and Code Sharing for common files (#9581) * Added ExpoSQLiteAdapter and Code Sharing for common files * changes to naming and updates regarding feedback from Draft PR * remove local changes from .vscode and package.json * Apply feedback from Caleb's and Chris's Code Review * remove unused imports and warn about db errors * added @types/react-native-sqlite-storage as devDependency for improved typing * Apply feedback from Chris and fix the initial sync problem * re-order dependencies * Addressed feedback from Chris * Assign any type to Result from SQLResultSetRowList as defined in expo-sqlite docs * Seperate entrypoint for ExpoSQLiteAdapter * Apply feedback from Caleb & Chris, Remove unused variable and separate entrypoints * Remove unnecesary async, alphabetic order for import statements and npm packages, single transaction for batchSave * remove unnecessary casting * Removed version, size and description args from openDatabase as they are not being used * Removed version, size and description args from openDatabase and moved entrypoint files in folders * Use similar export patterns across SQLiteAdapter and ExpoSQLiteAdapter * log additional information as warning when transaction promise rejects * Remove bug from batchSave that prevented the catch block execution * Fix lint issues * added back expo-file-system, accidentally removed it * Update adapter constructor name to CommonSQLiteAdapter * default export for adapter, symmetric export pattern across adapters and different entrypoints * export commonly used constants from common/constants * Update import statement and remove [] as a param from test. SQlite does not support arrays Co-authored-by: Caleb Pollman commit 8096cd5bb2fc4d9146cec5d2409d7957b3fa7a69 Author: aws-amplify-bot Date: Tue May 3 20:38:07 2022 +0000 chore(release): update version.ts [ci skip] commit ce9c595fb317302fe7988f6be9c63adae1e12fd4 Author: aws-amplify-bot Date: Tue May 3 20:35:31 2022 +0000 chore(release): Publish [ci skip] - @aws-amplify/ui-angular@1.0.48 - @aws-amplify/ui-components@1.9.19 - @aws-amplify/ui-react@1.2.39 - @aws-amplify/ui-storybook@2.0.39 - @aws-amplify/ui-vue@1.1.33 - @aws-amplify/analytics@5.2.6 - @aws-amplify/api-graphql@2.3.3 - @aws-amplify/api-rest@2.0.39 - @aws-amplify/api@4.0.39 - @aws-amplify/auth@4.5.3 - aws-amplify-angular@6.0.39 - aws-amplify-react@5.1.22 - aws-amplify@4.3.21 - @aws-amplify/cache@4.0.41 - @aws-amplify/core@4.5.3 - @aws-amplify/datastore-storage-adapter@1.2.13 - @aws-amplify/datastore@3.11.0 - @aws-amplify/geo@1.3.2 - @aws-amplify/interactions@4.0.39 - @aws-amplify/predictions@4.0.39 - @aws-amplify/pubsub@4.4.0 - @aws-amplify/pushnotification@4.3.18 - @aws-amplify/storage@4.4.22 - @aws-amplify/xr@3.0.39 commit fa8d0088d20373b0514aaf665ea7795f1274fa17 Author: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Tue May 3 16:00:46 2022 -0400 chore: preparing release commit cb245afe58592829586b8cac8e4891eecc17bdd4 Author: thaddmt <68032955+thaddmt@users.noreply.github.com> Date: Tue May 3 10:13:22 2022 -0700 chore(geo): reduce number of integ spec cases (#9862) commit 6ae8d10569abf24559436a46e1723825e6472489 Author: Dane Pilcher Date: Tue May 3 10:13:05 2022 -0600 feat: rework error handler (#9861) * feat: add new error handler * feat: remove error handler return type * Update packages/datastore/src/sync/processors/sync.ts Co-authored-by: Jon Wire * Update packages/datastore/src/sync/processors/mutation.ts Co-authored-by: Jon Wire * Update packages/datastore/src/sync/processors/subscription.ts Co-authored-by: Jon Wire * Update packages/datastore/src/types.ts Co-authored-by: Jon Wire * Update packages/datastore/src/sync/utils.ts Co-authored-by: Jon Wire * style: move error map * fix: move subscription error handler up * style: fix tslint * fix: typo * fix: make error handler required in sync processors Co-authored-by: ArkamJ Co-authored-by: Jon Wire commit a22b9621230a6c23882e5ef375b7460288322ca1 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Apr 28 12:39:25 2022 -0400 Address feedback from Chris commit dd2e1c83042dd7cc1c20fb873d301176fb2bae38 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Apr 14 13:34:08 2022 -0400 Add more tests and change the location commit 634433ae8b159c20706d10e621414cd722ae2b5a Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Mon Apr 4 13:29:17 2022 -0400 Add initial unit tests for ExpoSQLiteAdapter commit 5fac5e8b861f872559dc1f87cad239397a04d28a Merge: 5b1946786 611cc7b4a Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Tue Mar 29 15:09:35 2022 -0400 Merge branch 'expo-sqlite-adapter-codesharing' of github.com:chintannp/amplify-js into expo-sqlite-adapter-codesharing commit 5b1946786707fed57518114c72eb3f1d03b659dc Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Mon Mar 28 11:42:26 2022 -0400 Removed version, size and description args from openDatabase and moved entrypoint files in folders commit 611cc7b4abba2d2d2bfee7429679305a86c38c0e Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Mon Mar 28 11:42:26 2022 -0400 Removed version, size and description args from openDatabase as they are not being used commit cb1be23f1afca05d2b0b7e761a59174d271e3ae5 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Mar 17 15:29:01 2022 -0400 remove unnecessary casting commit 980c8731d411719db33e9d8c5f0be561b9a78a3e Merge: 3b98401cc b65d51110 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Mar 16 15:34:21 2022 -0400 Add missing , to package.json commit 3b98401cc7b18a6547a18531d8a8509f140e308b Merge: 55c76e655 0de27687c Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Mar 16 15:30:05 2022 -0400 Merge branch 'main' into expo-sqlite-adapter-codesharing commit b65d51110f245430e323f18fe5500f431b72fa56 Merge: 55c76e655 0de27687c Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Mar 16 15:30:05 2022 -0400 Merge branch 'main' into expo-sqlite-adapter-codesharing commit 55c76e655de7b8777f9657f9ed943c4fe2e3a079 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Mar 16 15:18:37 2022 -0400 Remove unnecesary async, alphabetic order for import statements and npm packages, single transaction for batchSave commit 014501791777a19afb31223745bd50994980b702 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Mar 10 12:42:08 2022 -0500 Apply feedback from Caleb & Chris, Remove unused variable and separate entrypoints commit 26bed3c1f410a55bd8e504e1d79433dba83f2d17 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Mon Mar 7 12:35:45 2022 -0500 Seperate entrypoint for ExpoSQLiteAdapter commit 07f16c08f4442067b2eee84367e85713ec6729a5 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Mar 3 12:58:57 2022 -0500 Assign any type to Result from SQLResultSetRowList as defined in expo-sqlite docs commit 9f3b6664924d6e928b57d7e73950c075d49b2af0 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Mar 3 12:38:14 2022 -0500 Addressed feedback from Chris commit e47898706f099ed36862f110b235e9b87a5a956c Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Mar 2 12:38:28 2022 -0500 re-order dependencies commit 910fe948f8a7f4641d171d6d393395e2d8803663 Merge: ceb5c8ed2 6117e71ed Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Mar 2 12:36:11 2022 -0500 Merge branch 'main' into expo-sqlite-adapter-codesharing commit ceb5c8ed2425e9ec3dd3028ff1dbd28d7264e75f Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Mar 2 12:24:13 2022 -0500 Apply feedback from Chris and fix the initial sync problem commit 1ec72f301d1426cddb8ae1a33fffb0e892ffc7ce Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Feb 17 12:17:44 2022 -0500 added @types/react-native-sqlite-storage as devDependency for improved typing commit 47f827444c61adc1e6171dd1dca029c4adc4a837 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Thu Feb 17 09:59:53 2022 -0500 remove unused imports and warn about db errors commit 6a7640089074d8a2187ddd54f7805470496e2840 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Wed Feb 16 17:27:33 2022 -0500 Apply feedback from Caleb's and Chris's Code Review commit 9f318ea31ee1a0a29f6ab333fa1464c3ff28d268 Merge: c635f3e31 9a52c2b6a Author: Caleb Pollman Date: Fri Feb 11 17:08:45 2022 -0800 Merge branch 'main' into expo-sqlite-adapter-codesharing commit c635f3e317ad57109a7b0d92e37d0ae3a15a160c Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Fri Feb 11 17:59:34 2022 -0500 remove local changes from .vscode and package.json commit b212dd3ceb0c12c32071ebef4e19e70f97e7af1c Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Fri Feb 11 17:55:48 2022 -0500 changes to naming and updates regarding feedback from Draft PR commit 3560af9fa71f50aa254639e2077fb369cf5a8a96 Author: chintannp <88387035+chintannp@users.noreply.github.com> Date: Fri Feb 11 13:26:30 2022 -0500 Added ExpoSQLiteAdapter and Code Sharing for common files --- .circleci/config.yml | 598 +++++++-- .github/CODEOWNERS | 16 +- CONTRIBUTING.md | 4 +- docs/Gemfile.lock | 4 +- package.json | 5 +- .../amazon-cognito-identity-js/CHANGELOG.md | 22 + packages/amazon-cognito-identity-js/README.md | 2 +- .../amazon-cognito-identity-js/index.d.ts | 37 +- .../amazon-cognito-identity-js/package.json | 2 +- packages/amplify-ui-angular/CHANGELOG.md | 104 ++ packages/amplify-ui-angular/package.json | 4 +- packages/amplify-ui-components/CHANGELOG.md | 104 ++ packages/amplify-ui-components/package.json | 4 +- packages/amplify-ui-react/CHANGELOG.md | 104 ++ packages/amplify-ui-react/package.json | 4 +- packages/amplify-ui-storybook/CHANGELOG.md | 104 ++ packages/amplify-ui-storybook/package.json | 4 +- packages/amplify-ui-vue/CHANGELOG.md | 104 ++ packages/amplify-ui-vue/package.json | 4 +- packages/analytics/CHANGELOG.md | 114 ++ ...alytics-unit-test.ts => Analytics.test.ts} | 8 +- ....ts => AWSKinesisFirehoseProvider.test.ts} | 0 ...nit-test.ts => AWSKinesisProvider.test.ts} | 0 ...it-test.ts => AWSPinpointProvider.test.ts} | 0 ...t.ts => AmazonPersonalizeProvider.test.ts} | 0 .../__tests__/Providers/EventBuffer.test.ts | 67 + ...ntTracker-test.ts => EventTracker.test.ts} | 0 ...racker-test.ts => PageViewTracker.test.ts} | 0 ...r-rn-test.ts => SessionTracker-rn.test.ts} | 0 ...Tracker-test.ts => SessionTracker.test.ts} | 0 .../{utils-test.ts => utils.test.ts} | 0 packages/analytics/package.json | 6 +- packages/analytics/src/Analytics.ts | 18 +- .../analytics/src/Providers/EventBuffer.ts | 3 +- packages/analytics/src/index.ts | 2 +- packages/analytics/src/types/Analytics.ts | 3 + .../src/types/Providers/AWSKinesisProvider.ts | 5 + .../Providers/AmazonPersonalizeProvider.ts | 7 + packages/api-graphql/CHANGELOG.md | 107 ++ .../api-graphql/__tests__/GraphQLAPI-test.ts | 58 +- packages/api-graphql/package.json | 12 +- packages/api-graphql/src/GraphQLAPI.ts | 35 +- packages/api-graphql/src/types/index.ts | 1 + packages/api-rest/CHANGELOG.md | 110 ++ .../__tests__/RestClient-unit-test.ts | 25 +- packages/api-rest/package.json | 6 +- packages/api-rest/src/RestAPI.ts | 9 + packages/api-rest/src/RestClient.ts | 18 +- packages/api/CHANGELOG.md | 107 ++ packages/api/__tests__/API-test.ts | 34 + packages/api/package.json | 6 +- packages/api/src/API.ts | 9 +- packages/auth/CHANGELOG.md | 115 ++ packages/auth/__tests__/auth-unit-test.ts | 162 ++- packages/auth/__tests__/hosted-ui.test.ts | 11 +- packages/auth/package.json | 8 +- packages/auth/src/Auth.ts | 160 ++- packages/auth/src/Errors.ts | 3 + packages/auth/src/common/AuthErrorStrings.ts | 1 + packages/auth/src/types/Auth.ts | 9 + packages/auth/src/urlListener.ts | 6 +- packages/aws-amplify-angular/CHANGELOG.md | 107 ++ packages/aws-amplify-angular/package.json | 5 +- .../aws-amplify-react-native/CHANGELOG.md | 11 + .../aws-amplify-react-native/package.json | 2 +- .../src/Auth/ConfirmSignUp.tsx | 27 +- packages/aws-amplify-react/CHANGELOG.md | 104 ++ packages/aws-amplify-react/package.json | 4 +- packages/aws-amplify-vue/CHANGELOG.md | 11 + packages/aws-amplify-vue/package.json | 6 +- packages/aws-amplify/CHANGELOG.md | 107 ++ .../__tests__/withSSRContext-test.ts | 12 +- packages/aws-amplify/package.json | 26 +- packages/cache/CHANGELOG.md | 104 ++ packages/cache/package.json | 4 +- packages/core/CHANGELOG.md | 110 ++ packages/core/__tests__/Platform-test.ts | 13 + packages/core/__tests__/Signer-test.ts | 57 + .../__tests__/parseMobileHubConfig-test.ts | 1 + packages/core/package.json | 2 +- packages/core/src/Parser.ts | 2 + packages/core/src/Platform/index.ts | 4 +- packages/core/src/Platform/version.ts | 2 +- packages/core/src/Signer.ts | 14 +- .../datastore-storage-adapter/CHANGELOG.md | 117 ++ .../ExpoSQLiteAdapter/index.d.ts | 1 + .../ExpoSQLiteAdapter/index.js | 7 + .../SQLiteAdapter/index.d.ts | 1 + .../SQLiteAdapter/index.js | 7 + .../__tests__/SQLiteAdapter.test.ts | 433 ++----- .../__tests__/SQLiteUtils.test.ts | 2 +- .../__tests__/helpers.ts | 1 - .../datastore-storage-adapter/package.json | 12 +- .../ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts | 8 + .../ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts | 282 ++++ .../src/SQLiteAdapter/SQLiteAdapter.ts | 480 +------ .../src/SQLiteAdapter/SQLiteDatabase.ts | 102 +- .../src/SQLiteAdapter/types.ts | 31 - .../src/common/CommonSQLiteAdapter.ts | 479 +++++++ .../{SQLiteAdapter => common}/SQLiteUtils.ts | 35 +- .../src/common/constants.ts | 1 + .../src/common/types.ts | 29 + .../datastore-storage-adapter/tslint.json | 2 +- .../webpack.config.dev.js | 2 + .../webpack.config.js | 4 + packages/datastore/CHANGELOG.md | 147 +++ .../__tests__/AsyncStorageAdapter.test.ts | 27 +- packages/datastore/__tests__/DataStore.ts | 573 +++++++- .../__tests__/IndexedDBAdapter.test.ts | 274 +--- .../__tests__/authStrategies.test.ts | 4 +- .../datastore/__tests__/commonAdapterTests.ts | 370 ++++++ packages/datastore/__tests__/helpers.ts | 93 ++ packages/datastore/__tests__/mutation.test.ts | 116 +- .../datastore/__tests__/subscription.test.ts | 143 +- packages/datastore/__tests__/sync.test.ts | 186 ++- packages/datastore/__tests__/util.test.ts | 126 ++ packages/datastore/package.json | 17 +- .../authModeStrategies/multiAuthStrategy.ts | 45 +- packages/datastore/src/datastore/datastore.ts | 242 +++- .../storage/adapter/AsyncStorageAdapter.ts | 1 + .../src/storage/adapter/InMemoryStore.ts | 4 +- packages/datastore/src/sync/index.ts | 18 +- .../src/sync/processors/errorMaps.ts | 93 ++ .../datastore/src/sync/processors/mutation.ts | 37 +- .../src/sync/processors/subscription.ts | 66 +- .../datastore/src/sync/processors/sync.ts | 36 +- packages/datastore/src/types.ts | 43 +- packages/datastore/src/util.ts | 72 +- packages/geo/CHANGELOG.md | 104 ++ packages/geo/__tests__/util.test.ts | 35 + packages/geo/package.json | 4 +- packages/interactions/CHANGELOG.md | 110 ++ .../__tests__/Interactions-unit-test.ts | 1153 +++++------------ .../providers/AWSLexProvider-unit-test.ts | 475 +++++++ packages/interactions/package.json | 4 +- packages/interactions/src/Interactions.ts | 64 +- .../src/Providers/AWSLexProvider.ts | 59 +- packages/interactions/src/index.ts | 1 + packages/interactions/src/types/Provider.ts | 2 +- .../src/types/Providers/AWSLexProvider.ts | 11 + packages/interactions/src/types/index.ts | 1 + packages/predictions/CHANGELOG.md | 104 ++ packages/predictions/package.json | 6 +- packages/pubsub/CHANGELOG.md | 118 ++ .../AWSAppSyncRealTimeProvider.test.ts | 309 +++-- .../__tests__/ConnectionStateMonitor.tests.ts | 226 ++++ packages/pubsub/__tests__/PubSub-unit-test.ts | 138 ++ packages/pubsub/__tests__/helpers.ts | 198 ++- packages/pubsub/package.json | 8 +- .../AWSAppSyncRealTimeProvider/index.ts | 82 +- .../src/Providers/MqttOverWSProvider.ts | 45 +- .../constants.ts | 5 + packages/pubsub/src/index.ts | 3 + packages/pubsub/src/types/index.ts | 36 + .../src/utils/ConnectionStateMonitor.ts | 192 +++ .../utils/ReachabilityMonitor/index.native.ts | 5 + .../src/utils/ReachabilityMonitor/index.ts | 3 + packages/pushnotification/CHANGELOG.md | 104 ++ packages/pushnotification/package.json | 4 +- packages/storage/CHANGELOG.md | 114 ++ .../providers/AWSS3Provider-unit-test.ts | 139 +- .../AWSS3ProviderManagedUpload-unit-test.ts | 44 +- packages/storage/package.json | 6 +- .../storage/src/providers/AWSS3Provider.ts | 80 +- .../providers/AWSS3ProviderManagedUpload.ts | 164 ++- .../src/providers/axios-http-handler.ts | 21 +- packages/storage/src/types/AWSS3Provider.ts | 12 +- packages/xr/CHANGELOG.md | 104 ++ packages/xr/package.json | 4 +- 169 files changed, 9540 insertions(+), 2736 deletions(-) rename packages/analytics/__tests__/{Analytics-unit-test.ts => Analytics.test.ts} (97%) rename packages/analytics/__tests__/Providers/{AWSKinesisFirehoseProvider-unit-test.ts => AWSKinesisFirehoseProvider.test.ts} (100%) rename packages/analytics/__tests__/Providers/{AWSKinesisProvider-unit-test.ts => AWSKinesisProvider.test.ts} (100%) rename packages/analytics/__tests__/Providers/{AWSPinpointProvider-unit-test.ts => AWSPinpointProvider.test.ts} (100%) rename packages/analytics/__tests__/Providers/{AmazonPersonalizeProvider-unit-test.ts => AmazonPersonalizeProvider.test.ts} (100%) create mode 100644 packages/analytics/__tests__/Providers/EventBuffer.test.ts rename packages/analytics/__tests__/trackers/{EventTracker-test.ts => EventTracker.test.ts} (100%) rename packages/analytics/__tests__/trackers/{PageViewTracker-test.ts => PageViewTracker.test.ts} (100%) rename packages/analytics/__tests__/trackers/{SessionTracker-rn-test.ts => SessionTracker-rn.test.ts} (100%) rename packages/analytics/__tests__/trackers/{SessionTracker-test.ts => SessionTracker.test.ts} (100%) rename packages/analytics/__tests__/{utils-test.ts => utils.test.ts} (100%) create mode 100644 packages/analytics/src/types/Providers/AWSKinesisProvider.ts create mode 100644 packages/analytics/src/types/Providers/AmazonPersonalizeProvider.ts create mode 100644 packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.d.ts create mode 100644 packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.js create mode 100644 packages/datastore-storage-adapter/SQLiteAdapter/index.d.ts create mode 100644 packages/datastore-storage-adapter/SQLiteAdapter/index.js create mode 100644 packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts create mode 100644 packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts delete mode 100644 packages/datastore-storage-adapter/src/SQLiteAdapter/types.ts create mode 100644 packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts rename packages/datastore-storage-adapter/src/{SQLiteAdapter => common}/SQLiteUtils.ts (94%) create mode 100644 packages/datastore-storage-adapter/src/common/constants.ts create mode 100644 packages/datastore-storage-adapter/src/common/types.ts create mode 100644 packages/datastore/__tests__/commonAdapterTests.ts create mode 100644 packages/datastore/src/sync/processors/errorMaps.ts create mode 100644 packages/interactions/__tests__/providers/AWSLexProvider-unit-test.ts create mode 100644 packages/interactions/src/types/Providers/AWSLexProvider.ts create mode 100644 packages/pubsub/__tests__/ConnectionStateMonitor.tests.ts rename packages/pubsub/src/Providers/{AWSAppSyncRealTimeProvider => }/constants.ts (94%) create mode 100644 packages/pubsub/src/utils/ConnectionStateMonitor.ts create mode 100644 packages/pubsub/src/utils/ReachabilityMonitor/index.native.ts create mode 100644 packages/pubsub/src/utils/ReachabilityMonitor/index.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index ea8f171a6f5..64e6c0105c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,7 +25,7 @@ executors: js-test-executor: docker: - - image: cypress/included:9.4.1 + - image: cypress/included:8.7.0 - image: verdaccio/verdaccio auth: username: $DOCKERHUB_USERNAME @@ -37,12 +37,6 @@ executors: xcode: 13.2.1 resource_class: large - # For RN apps that need to be upgraded before using latest iOS/macOS - macos-executor-legacy: - macos: - xcode: 11.5.0 - resource_class: large - test_env_vars: &test_env_vars environment: NPM_REGISTRY: http://0.0.0.0:4873/ @@ -453,6 +447,13 @@ jobs: sample_name: with-authenticator spec: ui-amplify-authenticator browser: << parameters.browser >> + - integ_test_js: + test_name: 'Sign In after Sign Up' + framework: react + category: auth + sample_name: auto-signin-after-signup + spec: auto-signin-after-signup + browser: << parameters.browser >> integ_angular_auth: parameters: browser: @@ -832,6 +833,23 @@ jobs: spec: observe-query browser: << parameters.browser >> + integ_react_datastore_schema_drift: + parameters: + browser: + type: string + executor: js-test-executor + <<: *test_env_vars + working_directory: ~/amplify-js-samples-staging/samples/react/datastore/schema-drift + steps: + - prepare_test_env + - integ_test_js: + test_name: 'DataStore - Schema Drift' + framework: react + category: datastore + sample_name: schema-drift + spec: schema-drift + browser: << parameters.browser >> + integ_react_storage: parameters: browser: @@ -880,6 +898,22 @@ jobs: sample_name: multi-part-copy-with-progress spec: multi-part-copy-with-progress browser: << parameters.browser >> + integ_react_graphql_api: + parameters: + browser: + type: string + executor: js-test-executor + <<: *test_env_vars + working_directory: ~/amplify-js-samples-staging/samples/react/api/graphql + steps: + - prepare_test_env + - integ_test_js: + test_name: 'React GraphQL API' + framework: react + category: api + sample_name: graphql + spec: graphql + browser: << parameters.browser >> integ_react_storage_ui: executor: js-test-executor <<: *test_env_vars @@ -968,7 +1002,7 @@ jobs: spec: delete-user browser: << parameters.browser >> integ_rn_ios_storage: - executor: macos-executor-legacy + executor: macos-executor <<: *test_env_vars working_directory: ~/amplify-js-samples-staging/samples/react-native/storage/StorageApp steps: @@ -988,19 +1022,19 @@ jobs: steps: - integ_test_rn_ios - integ_rn_android_storage: - executor: macos-executor-legacy - <<: *test_env_vars - working_directory: ~/amplify-js-samples-staging/samples/react-native/storage/StorageApp - steps: - - integ_test_rn_android + # integ_rn_android_storage: + # executor: macos-executor + # <<: *test_env_vars + # working_directory: ~/amplify-js-samples-staging/samples/react-native/storage/StorageApp + # steps: + # - integ_test_rn_android - integ_rn_android_storage_multipart_progress: - executor: macos-executor-legacy - <<: *test_env_vars - working_directory: ~/amplify-js-samples-staging/samples/react-native/storage/MultiPartUploadWithProgress - steps: - - integ_test_rn_android + # integ_rn_android_storage_multipart_progress: + # executor: macos-executor + # <<: *test_env_vars + # working_directory: ~/amplify-js-samples-staging/samples/react-native/storage/MultiPartUploadWithProgress + # steps: + # - integ_test_rn_android integ_rn_ios_datastore_sqlite_adapter: executor: macos-executor @@ -1056,23 +1090,22 @@ jobs: sample_name: v2/<< parameters.scenario >>-v2 spec: << parameters.scenario >> browser: firefox - - integ_duplicate_packages: - parameters: - browser: - type: string - executor: js-test-executor - <<: *test_env_vars - working_directory: ~/amplify-js-samples-staging/samples/react/version-conflict/duplicate-packages - steps: - - prepare_test_env - - integ_test_js: - test_name: 'Duplicate Package Errors' - framework: react - category: version-conflict - sample_name: duplicate-packages - spec: duplicate-packages - browser: << parameters.browser >> + # integ_duplicate_packages: + # parameters: + # browser: + # type: string + # executor: js-test-executor + # <<: *test_env_vars + # working_directory: ~/amplify-js-samples-staging/samples/react/version-conflict/duplicate-packages + # steps: + # - prepare_test_env + # - integ_test_js: + # test_name: 'Duplicate Package Errors' + # framework: react + # category: version-conflict + # sample_name: duplicate-packages + # spec: duplicate-packages + # browser: << parameters.browser >> integ_auth_test_cypress_no_ui: working_directory: ~/amplify-js-samples-staging/ executor: js-test-executor @@ -1097,35 +1130,34 @@ jobs: category: geo sample_name: display-map spec: display-map - browser: << parameters.browser >> - - integ_test_js: - test_name: 'Marker Map' - framework: react - category: geo - sample_name: marker-map - spec: marker-map - browser: << parameters.browser >> - - integ_test_js: - test_name: 'Cluster Map' - framework: react - category: geo - sample_name: cluster-marker-map - spec: cluster-marker-map - browser: << parameters.browser >> - - integ_test_js: - test_name: 'Search Map' - framework: react - category: geo - sample_name: search-map - spec: search-map - browser: << parameters.browser >> + # Temp fix: + browser: chrome - integ_test_js: test_name: 'Search Outside Map' framework: react category: geo sample_name: search-outside-map spec: search-outside-map + # Temp fix: + browser: chrome + + integ_next_datastore_owner_auth: + parameters: + browser: + type: string + executor: js-test-executor + <<: *test_env_vars + working_directory: ~/amplify-js-samples-staging/samples/next/datastore/owner-based-default + steps: + - prepare_test_env + - integ_test_js: + test_name: 'next owner auth' + framework: next + category: datastore + sample_name: owner-based-default + spec: next-owner-based-default browser: << parameters.browser >> + deploy: executor: macos-executor working_directory: ~/amplify-js @@ -1196,6 +1228,12 @@ datastore_auth_scenarios: &datastore_auth_scenarios workflows: build_test_deploy: + # Tells CircleCI to skip this workflow when the pipeline is triggered by the scheduled source, + # i.e. when the canaries workflow executes + # https://circleci.com/docs/2.0/scheduled-pipelines/#workflows-filtering + when: + not: + equal: [scheduled_pipeline, << pipeline.trigger_source >>] jobs: - build - integ_setup: @@ -1219,6 +1257,15 @@ workflows: - build filters: <<: *releasable_branches + - integ_react_graphql_api: + requires: + - integ_setup + - build + filters: + <<: *releasable_branches + matrix: + parameters: + <<: *test_browsers - integ_angular_auth: requires: - integ_setup @@ -1399,6 +1446,15 @@ workflows: matrix: parameters: <<: *test_browsers + - integ_react_datastore_schema_drift: + requires: + - integ_setup + - build + filters: + <<: *releasable_branches + matrix: + parameters: + <<: *test_browsers - integ_react_storage: requires: - integ_setup @@ -1492,24 +1548,24 @@ workflows: - build filters: <<: *releasable_branches - - integ_rn_android_storage: - requires: - - integ_setup - - build - filters: - <<: *releasable_branches - - integ_rn_android_storage_multipart_progress: - requires: - - integ_setup - - build - filters: - <<: *releasable_branches - - integ_rn_ios_datastore_sqlite_adapter: - requires: - - integ_setup - - build - filters: - <<: *releasable_branches + # - integ_rn_android_storage: + # requires: + # - integ_setup + # - build + # filters: + # <<: *releasable_branches + # - integ_rn_android_storage_multipart_progress: + # requires: + # - integ_setup + # - build + # filters: + # <<: *releasable_branches + # - integ_rn_ios_datastore_sqlite_adapter: + # requires: + # - integ_setup + # - build + # filters: + # <<: *releasable_branches - integ_datastore_auth: requires: - integ_setup @@ -1528,7 +1584,16 @@ workflows: matrix: parameters: <<: *datastore_auth_scenarios - - integ_duplicate_packages: + # - integ_duplicate_packages: + # requires: + # - integ_setup + # - build + # filters: + # <<: *releasable_branches + # matrix: + # parameters: + # <<: *test_browsers + - integ_react_geo: requires: - integ_setup - build @@ -1537,7 +1602,7 @@ workflows: matrix: parameters: <<: *test_browsers - - integ_react_geo: + - integ_next_datastore_owner_auth: requires: - integ_setup - build @@ -1567,6 +1632,7 @@ workflows: - integ_react_datastore_consecutive_saves_v2 - integ_react_datastore_observe_query - integ_react_datastore_observe_query_v2 + - integ_react_datastore_schema_drift - integ_react_storage - integ_react_storage_multipart_progress - integ_react_storage_copy @@ -1582,15 +1648,17 @@ workflows: - integ_vue_auth - integ_rn_ios_storage - integ_rn_ios_storage_multipart_progress - - integ_rn_android_storage_multipart_progress + # - integ_rn_android_storage_multipart_progress - integ_rn_ios_push_notifications - - integ_rn_android_storage - - integ_rn_ios_datastore_sqlite_adapter + # - integ_rn_android_storage + # - integ_rn_ios_datastore_sqlite_adapter - integ_datastore_auth - integ_datastore_auth_v2 - - integ_duplicate_packages + # - integ_duplicate_packages - integ_auth_test_cypress_no_ui + - integ_react_graphql_api - integ_react_geo + - integ_next_datastore_owner_auth - post_release: filters: branches: @@ -1598,3 +1666,367 @@ workflows: - release requires: - deploy + # Scheduled smoke test workflow + # Jobs are pulled from the getting-started-smoke-test inline orb defined below + canaries: + when: + and: + - equal: [scheduled_pipeline, << pipeline.trigger_source >>] + - equal: [canaries, << pipeline.schedule.name >>] + jobs: + ## Web + # React + - getting-started-smoke-test/web: + name: React - latest + npx-command: create-react-app + framework: react + main-file-path: src/App.js + build-dir: build + - getting-started-smoke-test/web: + name: React - next + npx-command: create-react-app@next + framework: react + main-file-path: src/App.js + build-dir: build + # Next + - getting-started-smoke-test/web: + name: Next.js - latest + npx-command: create-next-app + framework: nextjs + main-file-path: pages/_app.js + dev-start: dev + build-dir: build + ssr: true + - getting-started-smoke-test/web: + name: Next.js - next + npx-command: create-next-app@canary + framework: nextjs + main-file-path: pages/_app.js + dev-start: dev + build-dir: build + ssr: true + # Angular + - getting-started-smoke-test/web: + name: Angular - latest + npx-command: -p @angular/cli ng new --defaults true --force + framework: angular + main-file-path: src/main.ts + dev-port: 4200 + build-dir: dist + - getting-started-smoke-test/web: + name: Angular - next + npx-command: -p @angular/cli@next ng new --defaults true --force + framework: angular + main-file-path: src/main.ts + dev-port: 4200 + build-dir: dist + # Vue + - getting-started-smoke-test/web: + name: Vue - latest + npx-command: -p @vue/cli vue create --default --force --registry "https://registry.npmmirror.com" # npmmirror works better with vue create specifically in CCI + framework: vue + main-file-path: src/main.js + dev-start: serve + dev-port: 8080 + build-dir: dist + - getting-started-smoke-test/web: + name: Vue - next + npx-command: -p @vue/cli@next vue create --default --force --registry "https://registry.npmmirror.com" + framework: vue + main-file-path: src/main.js + dev-start: serve + dev-port: 8080 + build-dir: dist + ## Mobile + # RN CLI + - getting-started-smoke-test/rn-ios: + name: RN iOS - latest + framework: rn_ios + main-file-path: ./App.js + # TODO: delete next line after https://github.com/react-native-picker/picker/issues/425 is resolved + tag: 0.68.2 + - getting-started-smoke-test/rn-android: + name: RN Android - latest + framework: rn_android + main-file-path: ./App.js + # TODO: delete next line after https://github.com/react-native-picker/picker/issues/425 is resolved + tag: 0.68.2 + + # TODO: re-enable the following 2 jobs after https://github.com/react-native-picker/picker/issues/425 is resolved + # - getting-started-smoke-test/rn-ios: + # name: RN iOS - next + # framework: rn_ios + # main-file-path: ./App.js + # tag: next + # - getting-started-smoke-test/rn-android: + # name: RN Android - next + # framework: rn_android + # main-file-path: ./App.js + # tag: next + # Expo + - getting-started-smoke-test/expo: + name: Expo - latest + framework: expo_ios + main-file-path: ./App.js + - getting-started-smoke-test/expo: + name: Expo - next + framework: expo_ios + main-file-path: ./App.js + tag: next + +# Start Canary Orb +orbs: + getting-started-smoke-test: + executors: + web-executor: + docker: + - image: cimg/node:lts + resource_class: large + + android-executor: + machine: + image: android:202102-01 + resource_class: large + + common_env_vars: &common_env_vars + environment: + IMPORT_STATEMENT: import {Logger} from 'aws-amplify'; + LIBRARY_STATEMENT: Logger.LOG_LEVEL='DEBUG'; + MAX_WAIT_ON_TIMEOUT: 180000 # 3 minutes; how long we wait for the build to succeed before failing the job + + commands: + run-with-retry: + description: Run command with retry + parameters: + label: + description: Display name + type: string + pre-command: + description: This command will be executed once, but will not be retried + type: string + default: '' + command: + description: Command to run + type: string + retry-count: + description: Number of retry + type: integer + default: 3 + sleep: + description: Wait duration until next retry + type: integer + default: 5 + no_output_timeout: + description: Elapsed time the command can run without output + type: string + default: 10m + steps: + - run: + name: << parameters.label >> + command: | + retry() { + MAX_RETRY=<< parameters.retry-count >> + n=0 + until [ $n -ge $MAX_RETRY ] + do + bash -c "$@" && exit 0 + n=$[$n+1] + echo "retry $n of $MAX_RETRY" + sleep << parameters.sleep >> + done + if [ $n -ge $MAX_RETRY ]; then + echo "failed: ${@}" >&2 + exit 1 + fi + } + eval << parameters.pre-command >> + retry "<< parameters.command >>" + no_output_timeout: << parameters.no_output_timeout >> + + jobs: + web: + parameters: + framework: + type: string + npx-command: + type: string + npx-post: + type: string + default: '' + main-file-path: + type: string + dev-start: + type: string + default: start + dev-port: + type: integer + default: 3000 + build-dir: + type: string + ssr: + type: boolean + default: false + executor: web-executor + <<: *common_env_vars + working_directory: ~/project/amplify-getting-started-<< parameters.framework >> + steps: + - run-with-retry: + label: Scaffold App + pre-command: cd ../ + command: npx -y << parameters.npx-command >> amplify-getting-started-<< parameters.framework >> << parameters.npx-post >> + no_output_timeout: 2m + - run-with-retry: + label: Install AmplifyJS + command: npm i -S aws-amplify && npm i -g wait-on serve + - run: + name: Call Amplify library in code + command: | + echo "$IMPORT_STATEMENT" | cat - << parameters.main-file-path >> | tee << parameters.main-file-path >> + echo "$LIBRARY_STATEMENT" >> << parameters.main-file-path >> + - run-with-retry: + label: Run in Dev Mode + command: npm run << parameters.dev-start >> & wait-on http://localhost:<< parameters.dev-port >> -t $MAX_WAIT_ON_TIMEOUT + - unless: + condition: << parameters.ssr >> + steps: + - run-with-retry: + label: Run in Prod Mode + command: npm run build && (serve -n -s << parameters.build-dir >> -l 4000 & wait-on http://localhost:4000 -t $MAX_WAIT_ON_TIMEOUT) + - when: + condition: << parameters.ssr >> + steps: + - run-with-retry: + label: Run in Prod Mode + command: npm run build && (npm start & wait-on http://localhost:3000 -t $MAX_WAIT_ON_TIMEOUT) + + rn-ios: + parameters: + framework: + type: string + main-file-path: + type: string + tag: + type: string + default: latest + xcode-version: + type: string + default: 13.4.0 + ios-device: + type: string + default: iPhone 13 + macos: + xcode: << parameters.xcode-version >> + resource_class: large + working_directory: ~/amplify_getting_started_<< parameters.framework >> + steps: + - run-with-retry: + label: Scaffold App + pre-command: cd ../ && rm -rf amplify_getting_started_<< parameters.framework >> + command: npx react-native@<< parameters.tag >> init amplify_getting_started_<< parameters.framework >> --version << parameters.tag >> + no_output_timeout: 10m + - run-with-retry: + label: Install Amplify dependencies + command: npm install aws-amplify aws-amplify-react-native amazon-cognito-identity-js @react-native-community/netinfo @react-native-async-storage/async-storage @react-native-picker/picker + - run-with-retry: + label: Pod Install + command: npx pod-install + - run: + name: Call Amplify library in code + command: | + echo "$IMPORT_STATEMENT" | cat - << parameters.main-file-path >> | tee << parameters.main-file-path >> + echo "$LIBRARY_STATEMENT" >> << parameters.main-file-path >> + - run: + background: true + name: Start iOS simulator (background) + command: xcrun simctl boot "<< parameters.ios-device >>" || true + - run-with-retry: + label: Start App + command: npm run ios + + rn-android: + parameters: + framework: + type: string + main-file-path: + type: string + tag: + type: string + default: latest + executor: android-executor + working_directory: ~/amplify_getting_started_<< parameters.framework >> + steps: + - run: + name: Create avd + command: | + SYSTEM_IMAGES="system-images;android-29;default;x86" + sdkmanager "$SYSTEM_IMAGES" + echo "no" | avdmanager --verbose create avd -n test -k "$SYSTEM_IMAGES" + - run: + name: Launch emulator + command: | + emulator -avd test -delay-adb -verbose -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim + background: true + - run: + name: Generate cache key + command: | + find . -name 'build.gradle' | sort | xargs cat | + shasum | awk '{print $1}' > /tmp/gradle_cache_seed + - run-with-retry: + label: Scaffold App + pre-command: cd ../ && rm -rf amplify_getting_started_<< parameters.framework >> + command: npx react-native@<< parameters.tag >> init amplify_getting_started_<< parameters.framework >> --version << parameters.tag >> + no_output_timeout: 10m + - run-with-retry: + label: Install Amplify dependencies + command: npm install aws-amplify aws-amplify-react-native amazon-cognito-identity-js @react-native-community/netinfo @react-native-async-storage/async-storage @react-native-picker/picker + - run: + name: Call Amplify library in code + command: | + echo "$IMPORT_STATEMENT" | cat - << parameters.main-file-path >> | tee << parameters.main-file-path >> + echo "$LIBRARY_STATEMENT" >> << parameters.main-file-path >> + - run: + name: Wait for emulator to start + command: | + circle-android wait-for-boot + - run: + name: Disable emulator animations + command: | + adb shell settings put global window_animation_scale 0.0 + adb shell settings put global transition_animation_scale 0.0 + adb shell settings put global animator_duration_scale 0.0 + - run-with-retry: + label: Start App + command: npm run android + + expo: + parameters: + framework: + type: string + main-file-path: + type: string + tag: + type: string + default: latest + executor: web-executor + <<: *common_env_vars + working_directory: ~/amplify_getting_started_<< parameters.framework >> + steps: + - run-with-retry: + label: Install Expo + command: npm i -g expo-cli@<< parameters.tag >> wait-on + - run-with-retry: + label: Scaffold App + command: cd ../ && rm -rf amplify_getting_started_<< parameters.framework >> && expo-cli init amplify_getting_started_<< parameters.framework >> --yes + no_output_timeout: 5m + - run-with-retry: + label: Install Amplify dependencies + command: yarn add aws-amplify aws-amplify-react-native @react-native-community/netinfo @react-native-async-storage/async-storage @react-native-picker/picker + - run-with-retry: + label: Call Amplify library in code + command: | + echo "$IMPORT_STATEMENT" | cat - << parameters.main-file-path >> | tee << parameters.main-file-path >> + echo "$LIBRARY_STATEMENT" >> << parameters.main-file-path >> + - run-with-retry: + label: Start App + command: yarn web & wait-on http://localhost:19006 -t $MAX_WAIT_ON_TIMEOUT +# End Canary Orb diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a847dda17af..db8cf06c6f4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,20 +5,20 @@ /packages/amplify-ui-storybook @calebpollman /packages/amplify-ui-vue @calebpollman /packages/analytics @elorzafe -/packages/api @jamesaucode @david-mcafee -/packages/api-graphql @jamesaucode @david-mcafee -/packages/api-rest @jamesaucode @david-mcafee -/packages/auth @elorzafe @ashika01 @jamesaucode @nickarocho +/packages/api @svidgen @david-mcafee @manueliglesias @dpilch +/packages/api-graphql @svidgen @david-mcafee @manueliglesias @dpilch +/packages/api-rest @svidgen @david-mcafee @manueliglesias @dpilch +/packages/auth @elorzafe @ashika01 @jamesaucode /packages/aws-amplify @aws-amplify/amplify-js /packages/aws-amplify-angular @aws-amplify/amplify-js /packages/aws-amplify-react @aws-amplify/amplify-js -/packages/aws-amplify-react-native @calebpollman @nickarocho +/packages/aws-amplify-react-native @calebpollman /packages/aws-amplify-vue @aws-amplify/amplify-js /packages/cache @aws-amplify/amplify-js /packages/core @elorzafe @manueliglesias @iartemiev -/packages/datastore @svidgen @david-mcafee @manueliglesias @ArkamJ -/packages/datastore-storage-adapter @svidgen @david-mcafee @manueliglesias @ArkamJ -/packages/geo @TreTuna @thaddmt +/packages/datastore @svidgen @david-mcafee @manueliglesias @dpilch +/packages/datastore-storage-adapter @svidgen @david-mcafee @manueliglesias @dpilch +/packages/geo @thaddmt /packages/interactions @katiegoines /packages/predictions @elorzafe @stocaaro /packages/pubsub @stocaaro @elorzafe @ashika01 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37c76e07b08..6a906e27206 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,7 +120,7 @@ In your sample project, you can now link specific packages yarn link @aws-amplify/auth ``` -These tests are only necessary if you’re looking to contribute a Pull Request. If you’re just playing locally you don’t need them. However if you’re contributing a Pull Request for anything other than bug fixes it would be best to validate that first because depending on the scope of the change. +Passing unit tests are only necessary if you’re looking to contribute a pull request. If you’re just playing locally, you don’t need them. However, if you’re contributing a pull request for anything other than making a change to the documentation, fixing a formatting issue in the code (i.e., white space, missing semi-colons) or another task that does not impact the functionality of the code, you will need to validate your proposed changes with passing unit tests. **Using the setup-dev:react-native script to work with React-Native apps** @@ -139,7 +139,7 @@ To develop locally alongside a React-Native app, make sure to, npm run setup-dev:react-native -- --packages @aws-amplify/auth --target ~/path/to/your/rn/app/root ``` -> Note: This script runs a continious job in the newly opened tabs to watch, build and copy the changes unlike the usual linking method. +> Note: This script runs a continuous job in the newly opened tabs to watch, build and copy the changes unlike the usual linking method. The options `--packages` is used to specify single or multiple package names and the `--target` option is used to specify the path to your sample React-Native app. Optionally, you can use the shorthands flags `-p` and `-t` for packages and target path respectively. diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 453c25d9ad1..5d989c4cfb6 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -232,7 +232,7 @@ GEM jekyll-seo-tag (~> 2.1) minitest (5.15.0) multipart-post (2.1.1) - nokogiri (1.13.4) + nokogiri (1.13.6) mini_portile2 (~> 2.8.0) racc (~> 1.4) octokit (4.22.0) @@ -265,7 +265,7 @@ GEM thread_safe (0.3.6) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.9) + tzinfo (1.2.10) thread_safe (~> 0.1) unf (0.1.4) unf_ext diff --git a/package.json b/package.json index ad9f504cb78..fa70fe9bff6 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "devDependencies": { "@babel/cli": "7.17.0", "@babel/core": "7.17.2", + "@types/lodash": "4.14.182", "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "@types/jest": "^24.0.18", @@ -105,7 +106,9 @@ "@babel/traverse": "7.17.9", "@types/react": "16.9.10", "terser": "4.6.7", - "npm-packlist": "1.1.12" + "npm-packlist": "1.1.12", + "error-stack-parser": "2.0.6", + "@types/minimatch": "3.0.5" }, "bundlewatch": { "files": [ diff --git a/packages/amazon-cognito-identity-js/CHANGELOG.md b/packages/amazon-cognito-identity-js/CHANGELOG.md index c8d70bf84d6..af048f36790 100644 --- a/packages/amazon-cognito-identity-js/CHANGELOG.md +++ b/packages/amazon-cognito-identity-js/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [5.2.10](https://github.com/aws-amplify/amplify-js/compare/amazon-cognito-identity-js@5.2.9...amazon-cognito-identity-js@5.2.10) (2022-07-07) + + +### Bug Fixes + +* **amazon-cognito-identity-js:** Missing cognito user challenge name … ([#10047](https://github.com/aws-amplify/amplify-js/issues/10047)) ([de0441b](https://github.com/aws-amplify/amplify-js/commit/de0441b4fa67409ccbc630c42890e2c58ee779fb)), closes [#6974](https://github.com/aws-amplify/amplify-js/issues/6974) + + + + + +## [5.2.9](https://github.com/aws-amplify/amplify-js/compare/amazon-cognito-identity-js@5.2.8...amazon-cognito-identity-js@5.2.9) (2022-05-23) + + +### Bug Fixes + +* docs for amazon-cognito-identity-js ([#9909](https://github.com/aws-amplify/amplify-js/issues/9909)) ([08e01b1](https://github.com/aws-amplify/amplify-js/commit/08e01b1c09cfab73f2eb1b1b18fe1a696e2a028f)) + + + + + ## [5.2.8](https://github.com/aws-amplify/amplify-js/compare/amazon-cognito-identity-js@5.2.7...amazon-cognito-identity-js@5.2.8) (2022-03-10) **Note:** Version bump only for package amazon-cognito-identity-js diff --git a/packages/amazon-cognito-identity-js/README.md b/packages/amazon-cognito-identity-js/README.md index ce20854f076..f3f6295a5b5 100644 --- a/packages/amazon-cognito-identity-js/README.md +++ b/packages/amazon-cognito-identity-js/README.md @@ -478,7 +478,7 @@ cognitoUser.forgotPassword({ //Optional automatic callback inputVerificationCode: function(data) { console.log('Code sent to: ' + data); - var code = document.getElementById('code').value; + var verificationCode = document.getElementById('code').value; var newPassword = document.getElementById('new_password').value; cognitoUser.confirmPassword(verificationCode, newPassword, { onSuccess() { diff --git a/packages/amazon-cognito-identity-js/index.d.ts b/packages/amazon-cognito-identity-js/index.d.ts index 7f4d0451a33..e5d6e99d12f 100644 --- a/packages/amazon-cognito-identity-js/index.d.ts +++ b/packages/amazon-cognito-identity-js/index.d.ts @@ -24,11 +24,20 @@ declare module 'amazon-cognito-identity-js' { userAttributes: any, requiredAttributes: any ) => void; - mfaRequired?: (challengeName: any, challengeParameters: any) => void; - totpRequired?: (challengeName: any, challengeParameters: any) => void; + mfaRequired?: ( + challengeName: ChallengeName, + challengeParameters: any + ) => void; + totpRequired?: ( + challengeName: ChallengeName, + challengeParameters: any + ) => void; customChallenge?: (challengeParameters: any) => void; - mfaSetup?: (challengeName: any, challengeParameters: any) => void; - selectMFAType?: (challengeName: any, challengeParameters: any) => void; + mfaSetup?: (challengeName: ChallengeName, challengeParameters: any) => void; + selectMFAType?: ( + challengeName: ChallengeName, + challengeParameters: any + ) => void; } export interface IMfaSettings { @@ -67,9 +76,19 @@ declare module 'amazon-cognito-identity-js' { clientMetadata: Record; } + export type ChallengeName = + | 'CUSTOM_CHALLENGE' + | 'MFA_SETUP' + | 'NEW_PASSWORD_REQUIRED' + | 'SELECT_MFA_TYPE' + | 'SMS_MFA' + | 'SOFTWARE_TOKEN_MFA'; + export class CognitoUser { constructor(data: ICognitoUserData); + challengeName?: ChallengeName; + public setSignInUserSession(signInUserSession: CognitoUserSession): void; public getSignInUserSession(): CognitoUserSession | null; public getUsername(): string; @@ -246,8 +265,14 @@ declare module 'amazon-cognito-identity-js' { callbacks: { onSuccess: (session: CognitoUserSession) => void; onFailure: (err: any) => void; - mfaRequired?: (challengeName: any, challengeParameters: any) => void; - totpRequired?: (challengeName: any, challengeParameters: any) => void; + mfaRequired?: ( + challengeName: ChallengeName, + challengeParameters: any + ) => void; + totpRequired?: ( + challengeName: ChallengeName, + challengeParameters: any + ) => void; } ): void; } diff --git a/packages/amazon-cognito-identity-js/package.json b/packages/amazon-cognito-identity-js/package.json index 9d4e2b383d0..d98e64c6007 100644 --- a/packages/amazon-cognito-identity-js/package.json +++ b/packages/amazon-cognito-identity-js/package.json @@ -1,7 +1,7 @@ { "name": "amazon-cognito-identity-js", "description": "Amazon Cognito Identity Provider JavaScript SDK", - "version": "5.2.8", + "version": "5.2.10", "author": { "name": "Amazon Web Services", "email": "aws@amazon.com", diff --git a/packages/amplify-ui-angular/CHANGELOG.md b/packages/amplify-ui-angular/CHANGELOG.md index 8ed946d4e16..4523ce2d147 100644 --- a/packages/amplify-ui-angular/CHANGELOG.md +++ b/packages/amplify-ui-angular/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.60](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.59...@aws-amplify/ui-angular@1.0.60) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.59](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.58...@aws-amplify/ui-angular@1.0.59) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.58](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.57...@aws-amplify/ui-angular@1.0.58) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.57](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.56...@aws-amplify/ui-angular@1.0.57) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.56](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.55...@aws-amplify/ui-angular@1.0.56) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.55](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.54...@aws-amplify/ui-angular@1.0.55) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.54](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.53...@aws-amplify/ui-angular@1.0.54) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.53](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.52...@aws-amplify/ui-angular@1.0.53) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.52](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.51...@aws-amplify/ui-angular@1.0.52) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.50...@aws-amplify/ui-angular@1.0.51) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.49...@aws-amplify/ui-angular@1.0.50) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.48...@aws-amplify/ui-angular@1.0.49) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + +## [1.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.47...@aws-amplify/ui-angular@1.0.48) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/ui-angular + + + + + ## [1.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-angular@1.0.46...@aws-amplify/ui-angular@1.0.47) (2022-04-14) **Note:** Version bump only for package @aws-amplify/ui-angular diff --git a/packages/amplify-ui-angular/package.json b/packages/amplify-ui-angular/package.json index afa029cfb78..b4302ffc51d 100644 --- a/packages/amplify-ui-angular/package.json +++ b/packages/amplify-ui-angular/package.json @@ -1,7 +1,7 @@ { "name": "@aws-amplify/ui-angular", "private": "true", - "version": "1.0.47", + "version": "1.0.60", "description": "Angular specific wrapper for @aws-amplify/ui-components", "publishConfig": { "access": "public" @@ -32,7 +32,7 @@ "dist/" ], "dependencies": { - "@aws-amplify/ui-components": "1.9.18" + "@aws-amplify/ui-components": "1.9.31" }, "devDependencies": { "@angular/compiler-cli": "^7.2.1", diff --git a/packages/amplify-ui-components/CHANGELOG.md b/packages/amplify-ui-components/CHANGELOG.md index b506e9bb135..208ac8cf0e9 100644 --- a/packages/amplify-ui-components/CHANGELOG.md +++ b/packages/amplify-ui-components/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.9.31](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.30...@aws-amplify/ui-components@1.9.31) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.30](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.29...@aws-amplify/ui-components@1.9.30) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.29](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.28...@aws-amplify/ui-components@1.9.29) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.28](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.27...@aws-amplify/ui-components@1.9.28) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.27](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.26...@aws-amplify/ui-components@1.9.27) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.26](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.25...@aws-amplify/ui-components@1.9.26) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.25](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.24...@aws-amplify/ui-components@1.9.25) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.24](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.23...@aws-amplify/ui-components@1.9.24) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.23](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.22...@aws-amplify/ui-components@1.9.23) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.22](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.21...@aws-amplify/ui-components@1.9.22) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.21](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.20...@aws-amplify/ui-components@1.9.21) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.20](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.19...@aws-amplify/ui-components@1.9.20) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + +## [1.9.19](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.18...@aws-amplify/ui-components@1.9.19) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/ui-components + + + + + ## [1.9.18](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-components@1.9.17...@aws-amplify/ui-components@1.9.18) (2022-04-14) **Note:** Version bump only for package @aws-amplify/ui-components diff --git a/packages/amplify-ui-components/package.json b/packages/amplify-ui-components/package.json index 297c30adde3..4885bbaed4b 100644 --- a/packages/amplify-ui-components/package.json +++ b/packages/amplify-ui-components/package.json @@ -1,7 +1,7 @@ { "name": "@aws-amplify/ui-components", "private": "true", - "version": "1.9.18", + "version": "1.9.31", "description": "Core Amplify UI Component Library", "module": "dist/index.mjs", "main": "dist/index.js", @@ -43,7 +43,7 @@ "uuid": "^8.2.0" }, "devDependencies": { - "@aws-amplify/auth": "4.5.2", + "@aws-amplify/auth": "4.6.4", "@stencil/angular-output-target": "^0.0.2", "@stencil/core": "1.15.0", "@stencil/eslint-plugin": "0.2.1", diff --git a/packages/amplify-ui-react/CHANGELOG.md b/packages/amplify-ui-react/CHANGELOG.md index cd90d1ff44d..288e7a1c456 100644 --- a/packages/amplify-ui-react/CHANGELOG.md +++ b/packages/amplify-ui-react/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.2.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.50...@aws-amplify/ui-react@1.2.51) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.49...@aws-amplify/ui-react@1.2.50) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.48...@aws-amplify/ui-react@1.2.49) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.47...@aws-amplify/ui-react@1.2.48) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.46...@aws-amplify/ui-react@1.2.47) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.45...@aws-amplify/ui-react@1.2.46) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.44...@aws-amplify/ui-react@1.2.45) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.43...@aws-amplify/ui-react@1.2.44) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.42...@aws-amplify/ui-react@1.2.43) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.41...@aws-amplify/ui-react@1.2.42) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.40...@aws-amplify/ui-react@1.2.41) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.39...@aws-amplify/ui-react@1.2.40) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + +## [1.2.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.38...@aws-amplify/ui-react@1.2.39) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/ui-react + + + + + ## [1.2.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-react@1.2.37...@aws-amplify/ui-react@1.2.38) (2022-04-14) **Note:** Version bump only for package @aws-amplify/ui-react diff --git a/packages/amplify-ui-react/package.json b/packages/amplify-ui-react/package.json index fa1ebc0c850..4276e0afd07 100755 --- a/packages/amplify-ui-react/package.json +++ b/packages/amplify-ui-react/package.json @@ -2,7 +2,7 @@ "name": "@aws-amplify/ui-react", "private": "true", "sideEffects": false, - "version": "1.2.38", + "version": "1.2.51", "description": "React specific wrapper for @aws-amplify/ui-components", "publishConfig": { "access": "public" @@ -33,7 +33,7 @@ "typescript": "^3.3.4000" }, "dependencies": { - "@aws-amplify/ui-components": "1.9.18" + "@aws-amplify/ui-components": "1.9.31" }, "peerDependencies": { "react": ">= 16.7.0", diff --git a/packages/amplify-ui-storybook/CHANGELOG.md b/packages/amplify-ui-storybook/CHANGELOG.md index adf26abfc98..d083a9bf4de 100644 --- a/packages/amplify-ui-storybook/CHANGELOG.md +++ b/packages/amplify-ui-storybook/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.50...@aws-amplify/ui-storybook@2.0.51) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.49...@aws-amplify/ui-storybook@2.0.50) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.48...@aws-amplify/ui-storybook@2.0.49) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.47...@aws-amplify/ui-storybook@2.0.48) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.46...@aws-amplify/ui-storybook@2.0.47) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.45...@aws-amplify/ui-storybook@2.0.46) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.44...@aws-amplify/ui-storybook@2.0.45) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.43...@aws-amplify/ui-storybook@2.0.44) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.42...@aws-amplify/ui-storybook@2.0.43) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.41...@aws-amplify/ui-storybook@2.0.42) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.40...@aws-amplify/ui-storybook@2.0.41) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.39...@aws-amplify/ui-storybook@2.0.40) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + +## [2.0.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.38...@aws-amplify/ui-storybook@2.0.39) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/ui-storybook + + + + + ## [2.0.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-storybook@2.0.37...@aws-amplify/ui-storybook@2.0.38) (2022-04-14) **Note:** Version bump only for package @aws-amplify/ui-storybook diff --git a/packages/amplify-ui-storybook/package.json b/packages/amplify-ui-storybook/package.json index f4d7e6a39b0..a069ea3774b 100644 --- a/packages/amplify-ui-storybook/package.json +++ b/packages/amplify-ui-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/ui-storybook", - "version": "2.0.38", + "version": "2.0.51", "private": true, "dependencies": { "@aws-amplify/ui-react": "0.2.38", @@ -10,7 +10,7 @@ "@types/node": "^12.0.0", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", - "aws-amplify": "4.3.20", + "aws-amplify": "4.3.33", "react": "^16.12.0", "react-app-polyfill": "^1.0.6", "react-dom": "^16.12.0", diff --git a/packages/amplify-ui-vue/CHANGELOG.md b/packages/amplify-ui-vue/CHANGELOG.md index 7ee82beb744..6e55a0eccc5 100644 --- a/packages/amplify-ui-vue/CHANGELOG.md +++ b/packages/amplify-ui-vue/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.1.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.44...@aws-amplify/ui-vue@1.1.45) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.43...@aws-amplify/ui-vue@1.1.44) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.42...@aws-amplify/ui-vue@1.1.43) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.41...@aws-amplify/ui-vue@1.1.42) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.40...@aws-amplify/ui-vue@1.1.41) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.39...@aws-amplify/ui-vue@1.1.40) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.38...@aws-amplify/ui-vue@1.1.39) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.37...@aws-amplify/ui-vue@1.1.38) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.37](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.36...@aws-amplify/ui-vue@1.1.37) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.36](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.35...@aws-amplify/ui-vue@1.1.36) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.35](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.34...@aws-amplify/ui-vue@1.1.35) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.34](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.33...@aws-amplify/ui-vue@1.1.34) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + +## [1.1.33](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.32...@aws-amplify/ui-vue@1.1.33) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/ui-vue + + + + + ## [1.1.32](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/ui-vue@1.1.31...@aws-amplify/ui-vue@1.1.32) (2022-04-14) **Note:** Version bump only for package @aws-amplify/ui-vue diff --git a/packages/amplify-ui-vue/package.json b/packages/amplify-ui-vue/package.json index 6d387a5bab8..25f4329e3b9 100644 --- a/packages/amplify-ui-vue/package.json +++ b/packages/amplify-ui-vue/package.json @@ -2,7 +2,7 @@ "name": "@aws-amplify/ui-vue", "private": "true", "sideEffects": true, - "version": "1.1.32", + "version": "1.1.45", "description": "Vue specific wrapper for @aws-amplify/ui-components", "publishConfig": { "access": "public" @@ -18,7 +18,7 @@ "url": "https://github.com/aws-amplify/amplify-js.git" }, "dependencies": { - "@aws-amplify/ui-components": "1.9.18" + "@aws-amplify/ui-components": "1.9.31" }, "devDependencies": { "rimraf": "^3.0.2" diff --git a/packages/analytics/CHANGELOG.md b/packages/analytics/CHANGELOG.md index cceb8a25aad..12fcbd0b1d0 100644 --- a/packages/analytics/CHANGELOG.md +++ b/packages/analytics/CHANGELOG.md @@ -3,6 +3,120 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [5.2.18](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.17...@aws-amplify/analytics@5.2.18) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.17](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.16...@aws-amplify/analytics@5.2.17) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.16](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.15...@aws-amplify/analytics@5.2.16) (2022-08-16) + + +### Reverts + +* Revert "kinesis fix" ([88f118e](https://github.com/aws-amplify/amplify-js/commit/88f118e1340c38ba237362644035b4d7c9f72557)) +* Revert "update personalize type to accomodate string" ([4e0e22b](https://github.com/aws-amplify/amplify-js/commit/4e0e22bd733aac715681953dac90440a44fd49bd)) + + + + + +## [5.2.15](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.14...@aws-amplify/analytics@5.2.15) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.14](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.13...@aws-amplify/analytics@5.2.14) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.13](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.12...@aws-amplify/analytics@5.2.13) (2022-07-21) + + +### Bug Fixes + +* Update AmazonPersonalizeProvider Analytics typings ([#10076](https://github.com/aws-amplify/amplify-js/issues/10076)) ([b7ad126](https://github.com/aws-amplify/amplify-js/commit/b7ad1260eb1bc6611cd902e7b8b3e58066ef22b4)) + + + + + +## [5.2.12](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.11...@aws-amplify/analytics@5.2.12) (2022-07-07) + + +### Bug Fixes + +* **analytics:** Buffer limit should be adhered to ([#10015](https://github.com/aws-amplify/amplify-js/issues/10015)) ([3dd9035](https://github.com/aws-amplify/amplify-js/commit/3dd903573843f6f53251d118a11de7dacd9edddd)) + + + + + +## [5.2.11](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.10...@aws-amplify/analytics@5.2.11) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.9...@aws-amplify/analytics@5.2.10) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.8...@aws-amplify/analytics@5.2.9) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.7...@aws-amplify/analytics@5.2.8) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.6...@aws-amplify/analytics@5.2.7) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + +## [5.2.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.5...@aws-amplify/analytics@5.2.6) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/analytics + + + + + ## [5.2.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/analytics@5.2.4...@aws-amplify/analytics@5.2.5) (2022-04-14) **Note:** Version bump only for package @aws-amplify/analytics diff --git a/packages/analytics/__tests__/Analytics-unit-test.ts b/packages/analytics/__tests__/Analytics.test.ts similarity index 97% rename from packages/analytics/__tests__/Analytics-unit-test.ts rename to packages/analytics/__tests__/Analytics.test.ts index 7239ed3a8d0..ef1e88f0515 100644 --- a/packages/analytics/__tests__/Analytics-unit-test.ts +++ b/packages/analytics/__tests__/Analytics.test.ts @@ -108,8 +108,12 @@ describe('Analytics test', () => { await analytics.record({ name: 'event', - attributes: 'attributes', - metrics: 'metrics', + attributes: { + key: 'value', + }, + metrics: { + metric: 123, + }, }); expect(record_spyon).toBeCalled(); }); diff --git a/packages/analytics/__tests__/Providers/AWSKinesisFirehoseProvider-unit-test.ts b/packages/analytics/__tests__/Providers/AWSKinesisFirehoseProvider.test.ts similarity index 100% rename from packages/analytics/__tests__/Providers/AWSKinesisFirehoseProvider-unit-test.ts rename to packages/analytics/__tests__/Providers/AWSKinesisFirehoseProvider.test.ts diff --git a/packages/analytics/__tests__/Providers/AWSKinesisProvider-unit-test.ts b/packages/analytics/__tests__/Providers/AWSKinesisProvider.test.ts similarity index 100% rename from packages/analytics/__tests__/Providers/AWSKinesisProvider-unit-test.ts rename to packages/analytics/__tests__/Providers/AWSKinesisProvider.test.ts diff --git a/packages/analytics/__tests__/Providers/AWSPinpointProvider-unit-test.ts b/packages/analytics/__tests__/Providers/AWSPinpointProvider.test.ts similarity index 100% rename from packages/analytics/__tests__/Providers/AWSPinpointProvider-unit-test.ts rename to packages/analytics/__tests__/Providers/AWSPinpointProvider.test.ts diff --git a/packages/analytics/__tests__/Providers/AmazonPersonalizeProvider-unit-test.ts b/packages/analytics/__tests__/Providers/AmazonPersonalizeProvider.test.ts similarity index 100% rename from packages/analytics/__tests__/Providers/AmazonPersonalizeProvider-unit-test.ts rename to packages/analytics/__tests__/Providers/AmazonPersonalizeProvider.test.ts diff --git a/packages/analytics/__tests__/Providers/EventBuffer.test.ts b/packages/analytics/__tests__/Providers/EventBuffer.test.ts new file mode 100644 index 00000000000..537ebe8a06d --- /dev/null +++ b/packages/analytics/__tests__/Providers/EventBuffer.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import EventBuffer from '../../src/Providers/EventBuffer'; + +const DEFAULT_CONFIG = { + bufferSize: 1000, + flushSize: 100, + flushInterval: 5 * 1000, // 5s + resendLimit: 5, +}; + +const EVENT_OBJECT = { + params: { + event: { + eventId: 'event-id', + name: 'name', + attributes: 'attributes', + metrics: 'metrics', + session: {}, + immediate: false, + }, + timestamp: '2022-06-22T17:24:58Z', + config: { + appId: 'app-id', + endpointId: 'endpoint-id', + region: 'region', + resendLimit: 5, + }, + credentials: {}, + resendLimit: 5, + }, + handlers: { + resolve: jest.fn(), + reject: jest.fn(), + }, +}; + +describe('EventBuffer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('can be constructed', () => { + const buffer = new EventBuffer({}, DEFAULT_CONFIG); + expect(buffer).toBeDefined(); + }); + + test('does not allow buffer size to be exceeded', () => { + const config = { ...DEFAULT_CONFIG, bufferSize: 1 }; + const buffer = new EventBuffer({}, config); + buffer.push(EVENT_OBJECT); + buffer.push(EVENT_OBJECT); + expect(EVENT_OBJECT.handlers.reject).toBeCalledWith( + Error('Exceeded the size of analytics events buffer') + ); + }); +}); diff --git a/packages/analytics/__tests__/trackers/EventTracker-test.ts b/packages/analytics/__tests__/trackers/EventTracker.test.ts similarity index 100% rename from packages/analytics/__tests__/trackers/EventTracker-test.ts rename to packages/analytics/__tests__/trackers/EventTracker.test.ts diff --git a/packages/analytics/__tests__/trackers/PageViewTracker-test.ts b/packages/analytics/__tests__/trackers/PageViewTracker.test.ts similarity index 100% rename from packages/analytics/__tests__/trackers/PageViewTracker-test.ts rename to packages/analytics/__tests__/trackers/PageViewTracker.test.ts diff --git a/packages/analytics/__tests__/trackers/SessionTracker-rn-test.ts b/packages/analytics/__tests__/trackers/SessionTracker-rn.test.ts similarity index 100% rename from packages/analytics/__tests__/trackers/SessionTracker-rn-test.ts rename to packages/analytics/__tests__/trackers/SessionTracker-rn.test.ts diff --git a/packages/analytics/__tests__/trackers/SessionTracker-test.ts b/packages/analytics/__tests__/trackers/SessionTracker.test.ts similarity index 100% rename from packages/analytics/__tests__/trackers/SessionTracker-test.ts rename to packages/analytics/__tests__/trackers/SessionTracker.test.ts diff --git a/packages/analytics/__tests__/utils-test.ts b/packages/analytics/__tests__/utils.test.ts similarity index 100% rename from packages/analytics/__tests__/utils-test.ts rename to packages/analytics/__tests__/utils.test.ts diff --git a/packages/analytics/package.json b/packages/analytics/package.json index d7b50bf0cb6..9656b3024c9 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/analytics", - "version": "5.2.5", + "version": "5.2.18", "description": "Analytics category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -43,8 +43,8 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/cache": "4.0.40", - "@aws-amplify/core": "4.5.2", + "@aws-amplify/cache": "4.0.53", + "@aws-amplify/core": "4.7.2", "@aws-sdk/client-firehose": "3.6.1", "@aws-sdk/client-kinesis": "3.6.1", "@aws-sdk/client-personalize-events": "3.6.1", diff --git a/packages/analytics/src/Analytics.ts b/packages/analytics/src/Analytics.ts index 3873cb86cf1..8142e2257c9 100644 --- a/packages/analytics/src/Analytics.ts +++ b/packages/analytics/src/Analytics.ts @@ -27,6 +27,8 @@ import { AutoTrackSessionOpts, AutoTrackPageViewOpts, AutoTrackEventOpts, + PersonalizeAnalyticsEvent, + KinesisAnalyticsEvent, } from './types'; import { PageViewTracker, EventTracker, SessionTracker } from './trackers'; @@ -234,7 +236,10 @@ export class AnalyticsClass { * @param event - An object with the name of the event, attributes of the event and event metrics. * @param [provider] - name of the provider. */ - public async record(event: AnalyticsEvent, provider?: string); + public async record( + event: AnalyticsEvent | PersonalizeAnalyticsEvent | KinesisAnalyticsEvent, + provider?: string + ); /** * Record one analytic event and send it to Pinpoint * @deprecated Use the new syntax and pass in the event as an object instead. @@ -249,7 +254,11 @@ export class AnalyticsClass { metrics?: EventMetrics ); public async record( - event: string | AnalyticsEvent, + event: + | string + | AnalyticsEvent + | PersonalizeAnalyticsEvent + | KinesisAnalyticsEvent, providerOrAttributes?: string | EventAttributes, metrics?: EventMetrics ) { @@ -279,7 +288,10 @@ export class AnalyticsClass { return this.record(event, provider); } - private _sendEvent(params: { event: AnalyticsEvent; provider?: string }) { + private _sendEvent(params: { + event: AnalyticsEvent | PersonalizeAnalyticsEvent | KinesisAnalyticsEvent; + provider?: string; + }) { if (this._disabled) { logger.debug('Analytics has been disabled'); return Promise.resolve(); diff --git a/packages/analytics/src/Providers/EventBuffer.ts b/packages/analytics/src/Providers/EventBuffer.ts index 3f058209ee5..8b9363525f7 100644 --- a/packages/analytics/src/Providers/EventBuffer.ts +++ b/packages/analytics/src/Providers/EventBuffer.ts @@ -43,7 +43,8 @@ export default class EventsBuffer { } public push(event: EventObject) { - if (this._buffer > this._config.bufferSize) { + // if the buffer is currently at the configured limit, pushing would exceed it + if (this._buffer.length >= this._config.bufferSize) { logger.debug('Exceeded analytics events buffer size'); return event.handlers.reject( new Error('Exceeded the size of analytics events buffer') diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts index 0eecdb19856..664c1a16f18 100644 --- a/packages/analytics/src/index.ts +++ b/packages/analytics/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is located at diff --git a/packages/analytics/src/types/Analytics.ts b/packages/analytics/src/types/Analytics.ts index 38427360af1..412945ed556 100644 --- a/packages/analytics/src/types/Analytics.ts +++ b/packages/analytics/src/types/Analytics.ts @@ -92,3 +92,6 @@ export interface AnalyticsEvent { metrics?: EventMetrics; immediate?: boolean; } + +export { PersonalizeAnalyticsEvent } from './Providers/AmazonPersonalizeProvider'; +export { KinesisAnalyticsEvent } from './Providers/AWSKinesisProvider'; diff --git a/packages/analytics/src/types/Providers/AWSKinesisProvider.ts b/packages/analytics/src/types/Providers/AWSKinesisProvider.ts new file mode 100644 index 00000000000..422c5fea356 --- /dev/null +++ b/packages/analytics/src/types/Providers/AWSKinesisProvider.ts @@ -0,0 +1,5 @@ +export interface KinesisAnalyticsEvent { + data: object | string; + partitionKey: string; + streamName: string; +} diff --git a/packages/analytics/src/types/Providers/AmazonPersonalizeProvider.ts b/packages/analytics/src/types/Providers/AmazonPersonalizeProvider.ts new file mode 100644 index 00000000000..e1544f0fb33 --- /dev/null +++ b/packages/analytics/src/types/Providers/AmazonPersonalizeProvider.ts @@ -0,0 +1,7 @@ +export interface PersonalizeAnalyticsEvent { + eventType?: string; + userId?: string; + properties?: { + [key: string]: string; + }; +} diff --git a/packages/api-graphql/CHANGELOG.md b/packages/api-graphql/CHANGELOG.md index e6cf5ba622a..ad92b5bf3bf 100644 --- a/packages/api-graphql/CHANGELOG.md +++ b/packages/api-graphql/CHANGELOG.md @@ -3,6 +3,113 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.3.15](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.14...@aws-amplify/api-graphql@2.3.15) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.14](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.13...@aws-amplify/api-graphql@2.3.14) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.13](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.12...@aws-amplify/api-graphql@2.3.13) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.12](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.11...@aws-amplify/api-graphql@2.3.12) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.11](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.10...@aws-amplify/api-graphql@2.3.11) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.9...@aws-amplify/api-graphql@2.3.10) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.8...@aws-amplify/api-graphql@2.3.9) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.7...@aws-amplify/api-graphql@2.3.8) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.6...@aws-amplify/api-graphql@2.3.7) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.5...@aws-amplify/api-graphql@2.3.6) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.4...@aws-amplify/api-graphql@2.3.5) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + +## [2.3.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.3...@aws-amplify/api-graphql@2.3.4) (2022-05-12) + + +### Bug Fixes + +* **@aws-amplify/api:** graphql API.cancel fix ([#9578](https://github.com/aws-amplify/amplify-js/issues/9578)) ([a9ae27f](https://github.com/aws-amplify/amplify-js/commit/a9ae27f65e1a782321c0be87556f92d2ee432352)) + + + + + +## [2.3.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.2...@aws-amplify/api-graphql@2.3.3) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/api-graphql + + + + + ## [2.3.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-graphql@2.3.1...@aws-amplify/api-graphql@2.3.2) (2022-04-14) **Note:** Version bump only for package @aws-amplify/api-graphql diff --git a/packages/api-graphql/__tests__/GraphQLAPI-test.ts b/packages/api-graphql/__tests__/GraphQLAPI-test.ts index 8d9be0cc6ac..acb068b5b6e 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI-test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI-test.ts @@ -1237,7 +1237,63 @@ describe('API test', () => { }, }); }); - }); + + test('sends userAgent with suffix in request', async () => { + const spyonAuth = jest + .spyOn(Credentials, 'get') + .mockImplementationOnce(() => { + return new Promise((res, rej) => { + res('cred'); + }); + }); + + const spyon = jest + .spyOn(RestClient.prototype, 'post') + .mockImplementationOnce((url, init) => { + return new Promise((res, rej) => { + res({}); + }); + }); + + const api = new API(config); + const url = 'https://appsync.amazonaws.com', + region = 'us-east-2', + apiKey = 'secret_api_key', + variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, + userAgentSuffix = '/DataStore'; + api.configure({ + aws_appsync_graphqlEndpoint: url, + aws_appsync_region: region, + aws_appsync_authenticationType: 'API_KEY', + aws_appsync_apiKey: apiKey, + }); + + const headers = { + Authorization: null, + 'X-Api-Key': apiKey, + 'x-amz-user-agent': `${Constants.userAgent}${userAgentSuffix}`, + }; + + const body = { + query: getEventQuery, + variables, + }; + + const init = { + headers, + body, + signerServiceInfo: { + service: 'appsync', + region, + }, + cancellableToken: mockCancellableToken, + }; + let authToken: undefined; + + await api.graphql(graphqlOperation(GetEvent, variables, authToken, userAgentSuffix)); + + expect(spyon).toBeCalledWith(url, init); + }); describe('configure test', () => { test('without aws_project_region', () => { diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index 9d68c622a22..9209760932e 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/api-graphql", - "version": "2.3.2", + "version": "2.3.15", "description": "Api-graphql category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -44,11 +44,11 @@ "@types/zen-observable": "^0.8.0" }, "dependencies": { - "@aws-amplify/api-rest": "2.0.38", - "@aws-amplify/auth": "4.5.2", - "@aws-amplify/cache": "4.0.40", - "@aws-amplify/core": "4.5.2", - "@aws-amplify/pubsub": "4.3.2", + "@aws-amplify/api-rest": "2.0.51", + "@aws-amplify/auth": "4.6.4", + "@aws-amplify/cache": "4.0.53", + "@aws-amplify/core": "4.7.2", + "@aws-amplify/pubsub": "4.5.1", "graphql": "15.8.0", "uuid": "^3.2.1", "zen-observable-ts": "0.8.19" diff --git a/packages/api-graphql/src/GraphQLAPI.ts b/packages/api-graphql/src/GraphQLAPI.ts index ce6f0fdcd23..af12c364d20 100644 --- a/packages/api-graphql/src/GraphQLAPI.ts +++ b/packages/api-graphql/src/GraphQLAPI.ts @@ -22,8 +22,8 @@ import Observable from 'zen-observable-ts'; import { Amplify, ConsoleLogger as Logger, - Constants, Credentials, + getAmplifyUserAgent, INTERNAL_AWS_APPSYNC_REALTIME_PUBSUB_PROVIDER, } from '@aws-amplify/core'; import PubSub from '@aws-amplify/pubsub'; @@ -43,11 +43,13 @@ const logger = new Logger('GraphQLAPI'); export const graphqlOperation = ( query, variables = {}, - authToken?: string + authToken?: string, + userAgentSuffix?: string ) => ({ query, variables, authToken, + userAgentSuffix, }); /** @@ -224,7 +226,13 @@ export class GraphQLAPIClass { * @returns An Observable if the query is a subscription query, else a promise of the graphql result. */ graphql( - { query: paramQuery, variables = {}, authMode, authToken }: GraphQLOptions, + { + query: paramQuery, + variables = {}, + authMode, + authToken, + userAgentSuffix, + }: GraphQLOptions, additionalHeaders?: { [key: string]: string } ): Observable> | Promise> { const query = @@ -233,7 +241,7 @@ export class GraphQLAPIClass { : parse(print(paramQuery)); const [operationDef = {}] = query.definitions.filter( - (def) => def.kind === 'OperationDefinition' + def => def.kind === 'OperationDefinition' ); const { operation: operationType } = operationDef as OperationDefinitionNode; @@ -251,7 +259,7 @@ export class GraphQLAPIClass { const cancellableToken = this._api.getCancellableToken(); const initParams = { cancellableToken }; const responsePromise = this._graphql( - { query, variables, authMode }, + { query, variables, authMode, userAgentSuffix }, headers, initParams ); @@ -268,7 +276,7 @@ export class GraphQLAPIClass { } private async _graphql( - { query, variables, authMode }: GraphQLOptions, + { query, variables, authMode, userAgentSuffix }: GraphQLOptions, additionalHeaders = {}, initParams = {} ): Promise> { @@ -294,7 +302,7 @@ export class GraphQLAPIClass { ...(await graphql_headers({ query, variables })), ...additionalHeaders, ...(!customGraphqlEndpoint && { - [USER_AGENT_HEADER]: Constants.userAgent, + [USER_AGENT_HEADER]: getAmplifyUserAgent(userAgentSuffix), }), }; @@ -369,6 +377,15 @@ export class GraphQLAPIClass { return this._api.cancel(request, message); } + /** + * Check if the request has a corresponding cancel token in the WeakMap. + * @params request - The request promise + * @return if the request has a corresponding cancel token. + */ + hasCancelToken(request: Promise) { + return this._api.hasCancelToken(request); + } + private _graphqlSubscribe( { query, @@ -412,14 +429,14 @@ export class GraphQLAPIClass { */ _ensureCredentials() { return this.Credentials.get() - .then((credentials) => { + .then(credentials => { if (!credentials) return false; const cred = this.Credentials.shear(credentials); logger.debug('set credentials for api', cred); return true; }) - .catch((err) => { + .catch(err => { logger.warn('ensure credentials error', err); return false; }); diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index 2a9d4455706..2700600459c 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -20,6 +20,7 @@ export interface GraphQLOptions { variables?: object; authMode?: keyof typeof GRAPHQL_AUTH_MODE; authToken?: string; + userAgentSuffix?: string; } export interface GraphQLResult { diff --git a/packages/api-rest/CHANGELOG.md b/packages/api-rest/CHANGELOG.md index 263a25cb7ed..0787d10793b 100644 --- a/packages/api-rest/CHANGELOG.md +++ b/packages/api-rest/CHANGELOG.md @@ -3,6 +3,116 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.50...@aws-amplify/api-rest@2.0.51) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.49...@aws-amplify/api-rest@2.0.50) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.48...@aws-amplify/api-rest@2.0.49) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.47...@aws-amplify/api-rest@2.0.48) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.46...@aws-amplify/api-rest@2.0.47) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.45...@aws-amplify/api-rest@2.0.46) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.44...@aws-amplify/api-rest@2.0.45) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.43...@aws-amplify/api-rest@2.0.44) (2022-06-18) + + +### Bug Fixes + +* update axios ([67316d7](https://github.com/aws-amplify/amplify-js/commit/67316d78fd829b9d4875a25d00719b175738e594)) + + + + + +## [2.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.42...@aws-amplify/api-rest@2.0.43) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.41...@aws-amplify/api-rest@2.0.42) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.40...@aws-amplify/api-rest@2.0.41) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + +## [2.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.39...@aws-amplify/api-rest@2.0.40) (2022-05-12) + + +### Bug Fixes + +* **@aws-amplify/api:** graphql API.cancel fix ([#9578](https://github.com/aws-amplify/amplify-js/issues/9578)) ([a9ae27f](https://github.com/aws-amplify/amplify-js/commit/a9ae27f65e1a782321c0be87556f92d2ee432352)) + + + + + +## [2.0.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.38...@aws-amplify/api-rest@2.0.39) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/api-rest + + + + + ## [2.0.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api-rest@2.0.37...@aws-amplify/api-rest@2.0.38) (2022-04-14) **Note:** Version bump only for package @aws-amplify/api-rest diff --git a/packages/api-rest/__tests__/RestClient-unit-test.ts b/packages/api-rest/__tests__/RestClient-unit-test.ts index 38331035d7d..aa4941ea9ce 100644 --- a/packages/api-rest/__tests__/RestClient-unit-test.ts +++ b/packages/api-rest/__tests__/RestClient-unit-test.ts @@ -452,6 +452,11 @@ describe('RestClient test', () => { }); describe('Cancel Token', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + const apiOptions = { headers: {}, endpoints: [ @@ -474,16 +479,24 @@ describe('RestClient test', () => { test('request non existent', () => { const restClient = new RestClient(apiOptions); - // if the request doesn't exist we can still say it is canceled successfully - expect( - restClient.cancel( - new Promise((req, res) => {}) - ) - ).toBeTruthy(); + expect(restClient.cancel(new Promise((req, res) => {}))).toBe(false); + }); + + test('request exist', () => { + const restClient = new RestClient(apiOptions); + const request = Promise.resolve(); + restClient.updateRequestToBeCancellable( + request, + restClient.getCancellableToken() + ); + expect(restClient.cancel(request)).toBe(true); }); test('happy case', () => { const restClient = new RestClient(apiOptions); + jest + .spyOn(RestClient.prototype, 'ajax') + .mockImplementationOnce(() => Promise.resolve()); const cancellableToken = restClient.getCancellableToken(); const request = restClient.ajax('url', 'method', { cancellableToken }); diff --git a/packages/api-rest/package.json b/packages/api-rest/package.json index f5e88eeb0ca..4a1f61f8ce5 100644 --- a/packages/api-rest/package.json +++ b/packages/api-rest/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/api-rest", - "version": "2.0.38", + "version": "2.0.51", "description": "Api-rest category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -41,8 +41,8 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/core": "4.5.2", - "axios": "0.21.4" + "@aws-amplify/core": "4.7.2", + "axios": "0.26.0" }, "jest": { "globals": { diff --git a/packages/api-rest/src/RestAPI.ts b/packages/api-rest/src/RestAPI.ts index 6828e37653d..0ed75253e00 100644 --- a/packages/api-rest/src/RestAPI.ts +++ b/packages/api-rest/src/RestAPI.ts @@ -281,6 +281,15 @@ export class RestAPIClass { return this._api.cancel(request, message); } + /** + * Check if the request has a corresponding cancel token in the WeakMap. + * @params request - The request promise + * @return if the request has a corresponding cancel token. + */ + hasCancelToken(request: Promise) { + return this._api.hasCancelToken(request); + } + /** * Getting endpoint for API * @param {string} apiName - The name of the api diff --git a/packages/api-rest/src/RestClient.ts b/packages/api-rest/src/RestClient.ts index 3d643a1574f..a9337d9c55b 100644 --- a/packages/api-rest/src/RestClient.ts +++ b/packages/api-rest/src/RestClient.ts @@ -294,8 +294,18 @@ export class RestClient { const source = this._cancelTokenMap.get(request); if (source) { source.cancel(message); + return true; } - return true; + return false; + } + + /** + * Check if the request has a corresponding cancel token in the WeakMap. + * @params request - The request promise + * @return if the request has a corresponding cancel token. + */ + hasCancelToken(request: Promise) { + return this._cancelTokenMap.has(request); } /** @@ -366,10 +376,8 @@ export class RestClient { /** private methods **/ private _signed(params, credentials, isAllResponse, { service, region }) { - const { - signerServiceInfo: signerServiceInfoParams, - ...otherParams - } = params; + const { signerServiceInfo: signerServiceInfoParams, ...otherParams } = + params; const endpoint_region: string = region || this._region || this._options.region; diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index d35b0331226..f6272d62325 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -3,6 +3,113 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.50...@aws-amplify/api@4.0.51) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.49...@aws-amplify/api@4.0.50) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.48...@aws-amplify/api@4.0.49) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.47...@aws-amplify/api@4.0.48) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.46...@aws-amplify/api@4.0.47) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.45...@aws-amplify/api@4.0.46) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.44...@aws-amplify/api@4.0.45) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.43...@aws-amplify/api@4.0.44) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.42...@aws-amplify/api@4.0.43) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.41...@aws-amplify/api@4.0.42) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.40...@aws-amplify/api@4.0.41) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/api + + + + + +## [4.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.39...@aws-amplify/api@4.0.40) (2022-05-12) + + +### Bug Fixes + +* **@aws-amplify/api:** graphql API.cancel fix ([#9578](https://github.com/aws-amplify/amplify-js/issues/9578)) ([a9ae27f](https://github.com/aws-amplify/amplify-js/commit/a9ae27f65e1a782321c0be87556f92d2ee432352)) + + + + + +## [4.0.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.38...@aws-amplify/api@4.0.39) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/api + + + + + ## [4.0.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/api@4.0.37...@aws-amplify/api@4.0.38) (2022-04-14) **Note:** Version bump only for package @aws-amplify/api diff --git a/packages/api/__tests__/API-test.ts b/packages/api/__tests__/API-test.ts index ecc84897591..b0a923d3dc5 100644 --- a/packages/api/__tests__/API-test.ts +++ b/packages/api/__tests__/API-test.ts @@ -84,4 +84,38 @@ describe('API test', () => { const api = new API(null); expect(await api.graphql({ query: 'query' })).toBe('grapqhqlResponse'); }); + + describe('cancel', () => { + test('cancel RestAPI request', async () => { + jest + .spyOn(GraphQLAPIClass.prototype, 'hasCancelToken') + .mockImplementation(() => false); + const restAPICancelSpy = jest + .spyOn(RestAPIClass.prototype, 'cancel') + .mockImplementation(() => true); + jest + .spyOn(RestAPIClass.prototype, 'hasCancelToken') + .mockImplementation(() => true); + const api = new API(null); + const request = Promise.resolve(); + expect(api.cancel(request)).toBe(true); + expect(restAPICancelSpy).toHaveBeenCalled(); + }); + + test('cancel GraphQLAPI request', async () => { + jest + .spyOn(GraphQLAPIClass.prototype, 'hasCancelToken') + .mockImplementation(() => true); + const graphQLAPICancelSpy = jest + .spyOn(GraphQLAPIClass.prototype, 'cancel') + .mockImplementation(() => true); + jest + .spyOn(RestAPIClass.prototype, 'hasCancelToken') + .mockImplementation(() => false); + const api = new API(null); + const request = Promise.resolve(); + expect(api.cancel(request)).toBe(true); + expect(graphQLAPICancelSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/api/package.json b/packages/api/package.json index 8a35393e13b..f75d2978cf6 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/api", - "version": "4.0.38", + "version": "4.0.51", "description": "Api category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -51,8 +51,8 @@ "@types/zen-observable": "^0.8.0" }, "dependencies": { - "@aws-amplify/api-graphql": "2.3.2", - "@aws-amplify/api-rest": "2.0.38" + "@aws-amplify/api-graphql": "2.3.15", + "@aws-amplify/api-rest": "2.0.51" }, "jest": { "globals": { diff --git a/packages/api/src/API.ts b/packages/api/src/API.ts index 94d48c1c13b..e0477e4f24a 100644 --- a/packages/api/src/API.ts +++ b/packages/api/src/API.ts @@ -183,13 +183,18 @@ export class APIClass { return this._restApi.isCancel(error); } /** - * Cancels an inflight request + * Cancels an inflight request for either a GraphQL request or a Rest API request. * @param request - request to cancel * @param [message] - custom error message * @return If the request was cancelled */ cancel(request: Promise, message?: string): boolean { - return this._restApi.cancel(request, message); + if (this._restApi.hasCancelToken(request)) { + return this._restApi.cancel(request, message); + } else if (this._graphqlApi.hasCancelToken(request)) { + return this._graphqlApi.cancel(request, message); + } + return false; } /** diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 047289b67da..9719c0dc312 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -3,6 +3,121 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.6.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.6.3...@aws-amplify/auth@4.6.4) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.6.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.6.2...@aws-amplify/auth@4.6.3) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.6.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.6.1...@aws-amplify/auth@4.6.2) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.6.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.6.0...@aws-amplify/auth@4.6.1) (2022-08-01) + + +### Bug Fixes + +* **@aws-amplify/auth:** fix storage bug for auto sign in value ([#10139](https://github.com/aws-amplify/amplify-js/issues/10139)) ([06504e6](https://github.com/aws-amplify/amplify-js/commit/06504e649068f01b85392373fdf80e2ed2a6cada)) +* **auth:** Unauthenticated identity throws AuthError without user … ([#10090](https://github.com/aws-amplify/amplify-js/issues/10090)) ([2ac9035](https://github.com/aws-amplify/amplify-js/commit/2ac903516ec295fbf098f6a6644000177f315184)) + + + + + +# [4.6.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.10...@aws-amplify/auth@4.6.0) (2022-07-28) + + +### Features + +* **@aws-amplify/auth:** Auto sign in after sign up ([#10126](https://github.com/aws-amplify/amplify-js/issues/10126)) ([e54617f](https://github.com/aws-amplify/amplify-js/commit/e54617f2878244f0e391d2d49f5cd2e8a8c069f9)), closes [#6320](https://github.com/aws-amplify/amplify-js/issues/6320) [#3882](https://github.com/aws-amplify/amplify-js/issues/3882) [#3631](https://github.com/aws-amplify/amplify-js/issues/3631) [#6018](https://github.com/aws-amplify/amplify-js/issues/6018) + + + + + +## [4.5.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.9...@aws-amplify/auth@4.5.10) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.5.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.8...@aws-amplify/auth@4.5.9) (2022-07-07) + + +### Bug Fixes + +* **amazon-cognito-identity-js:** Missing cognito user challenge name … ([#10047](https://github.com/aws-amplify/amplify-js/issues/10047)) ([de0441b](https://github.com/aws-amplify/amplify-js/commit/de0441b4fa67409ccbc630c42890e2c58ee779fb)), closes [#6974](https://github.com/aws-amplify/amplify-js/issues/6974) +* Update Auth to import JS using named export ([#10033](https://github.com/aws-amplify/amplify-js/issues/10033)) ([11b537c](https://github.com/aws-amplify/amplify-js/commit/11b537c62fee74c04e4e3b72ba43a353ba5152c9)) + + + + + +## [4.5.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.7...@aws-amplify/auth@4.5.8) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.5.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.6...@aws-amplify/auth@4.5.7) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.5.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.5...@aws-amplify/auth@4.5.6) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.5.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.4...@aws-amplify/auth@4.5.5) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.5.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.3...@aws-amplify/auth@4.5.4) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + +## [4.5.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.2...@aws-amplify/auth@4.5.3) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/auth + + + + + ## [4.5.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/auth@4.5.1...@aws-amplify/auth@4.5.2) (2022-04-14) **Note:** Version bump only for package @aws-amplify/auth diff --git a/packages/auth/__tests__/auth-unit-test.ts b/packages/auth/__tests__/auth-unit-test.ts index e4093155b2d..c28bc8a4ffa 100644 --- a/packages/auth/__tests__/auth-unit-test.ts +++ b/packages/auth/__tests__/auth-unit-test.ts @@ -8,6 +8,7 @@ import { CognitoIdToken, CognitoAccessToken, NodeCallback, + ISignUpResult, } from 'amazon-cognito-identity-js'; const MAX_DEVICES: number = 60; @@ -319,6 +320,14 @@ const authOptionsWithHostedUIConfig: AuthOptions = { responseType: 'code', }, }; +const authOptionConfirmationLink: AuthOptions = { + userPoolId: 'awsUserPoolsId', + userPoolWebClientId: 'awsUserPoolsWebClientId', + region: 'region', + identityPoolId: 'awsCognitoIdentityPoolId', + mandatorySignIn: false, + signUpVerificationMethod: 'link', +}; const authOptionsWithClientMetadata: AuthOptions = { userPoolId: 'awsUserPoolsId', @@ -343,6 +352,13 @@ const userPool = new CognitoUserPool({ ClientId: authOptions.userPoolWebClientId, }); +const signUpResult: ISignUpResult = { + user: null, + userConfirmed: true, + userSub: 'userSub', + codeDeliveryDetails: null, +}; + const idToken = new CognitoIdToken({ IdToken: 'idToken' }); const accessToken = new CognitoAccessToken({ AccessToken: 'accessToken' }); @@ -558,6 +574,122 @@ describe('auth unit test', () => { }); }); + describe('autoSignInAfterSignUp', () => { + test('happy case auto confirm', async () => { + const spyon = jest + .spyOn(CognitoUserPool.prototype, 'signUp') + .mockImplementationOnce( + ( + username, + password, + signUpAttributeList, + validationData, + callback, + clientMetadata + ) => { + callback(null, signUpResult); + } + ); + const signInSpyon = jest.spyOn(CognitoUser.prototype, 'authenticateUser'); + const auth = new Auth(authOptions); + const attrs = { + username: 'username', + password: 'password', + attributes: { + email: 'email', + phone_number: 'phone_number', + otherAttrs: 'otherAttrs', + }, + autoSignIn: { enabled: true }, + }; + expect(await auth.signUp(attrs)).toBe(signUpResult); + expect(signInSpyon).toHaveBeenCalledTimes(1); + spyon.mockClear(); + signInSpyon.mockClear(); + }); + + test('happy case confirmation code', async () => { + const spyon = jest.spyOn(CognitoUserPool.prototype, 'signUp'); + const confirmSpyon = jest.spyOn( + CognitoUser.prototype, + 'confirmRegistration' + ); + const signInSpyon = jest.spyOn(CognitoUser.prototype, 'authenticateUser'); + const auth = new Auth(authOptions); + const attrs = { + username: 'username', + password: 'password', + attributes: { + email: 'email', + phone_number: 'phone_number', + otherAttrs: 'otherAttrs', + }, + autoSignIn: { enabled: true }, + }; + expect(await auth.signUp(attrs)).toBe('signUpResult'); + expect(await auth.confirmSignUp('username', 'code')).toBe('Success'); + expect(signInSpyon).toHaveBeenCalledTimes(1); + spyon.mockClear(); + confirmSpyon.mockClear(); + signInSpyon.mockClear(); + }); + + test('happy case confirmation link', async () => { + jest.useFakeTimers(); + const spyon = jest.spyOn(CognitoUserPool.prototype, 'signUp'); + const signInSpyon = jest.spyOn(CognitoUser.prototype, 'authenticateUser'); + const auth = new Auth(authOptionConfirmationLink); + const attrs = { + username: 'username', + password: 'password', + attributes: { + email: 'email', + phone_number: 'phone_number', + otherAttrs: 'otherAttrs', + }, + autoSignIn: { enabled: true }, + }; + expect(await auth.signUp(attrs)).toBe('signUpResult'); + jest.advanceTimersByTime(11000); + expect(signInSpyon).toHaveBeenCalledTimes(2); + spyon.mockClear(); + signInSpyon.mockClear(); + }); + + test('fail confirmation code', async () => { + const spyon = jest.spyOn(CognitoUserPool.prototype, 'signUp'); + const confirmSpyon = jest + .spyOn(CognitoUser.prototype, 'confirmRegistration') + .mockImplementationOnce( + (confirmationCode, forceAliasCreation, callback) => { + callback('err', null); + } + ); + const signInSpyon = jest.spyOn(CognitoUser.prototype, 'authenticateUser'); + const auth = new Auth(authOptions); + const attrs = { + username: 'username', + password: 'password', + attributes: { + email: 'email', + phone_number: 'phone_number', + otherAttrs: 'otherAttrs', + }, + autoSignIn: { enabled: true }, + }; + expect(await auth.signUp(attrs)).toBe('signUpResult'); + try { + await auth.confirmSignUp('username', 'code'); + } catch (e) { + expect(e).toBe('err'); + } + expect(signInSpyon).toHaveBeenCalledTimes(0); + spyon.mockClear(); + confirmSpyon.mockClear(); + signInSpyon.mockClear(); + }); + }); + describe('confirmSignUp', () => { test('happy case', async () => { const spyon = jest.spyOn(CognitoUser.prototype, 'confirmRegistration'); @@ -1011,7 +1143,7 @@ describe('auth unit test', () => { const spyon = jest .spyOn(CognitoUser.prototype, 'authenticateUser') .mockImplementationOnce((authenticationDetails, callback) => { - callback.mfaRequired('challengeName', 'challengeParam'); + callback.mfaRequired('SELECT_MFA_TYPE', 'challengeParam'); }); const auth = new Auth(authOptions); const user = new CognitoUser({ @@ -1019,7 +1151,7 @@ describe('auth unit test', () => { Pool: userPool, }); const userAfterSignedIn = Object.assign({}, user, { - challengeName: 'challengeName', + challengeName: 'SELECT_MFA_TYPE', challengeParam: 'challengeParam', }); @@ -1035,7 +1167,7 @@ describe('auth unit test', () => { const spyon = jest .spyOn(CognitoUser.prototype, 'authenticateUser') .mockImplementationOnce((authenticationDetails, callback) => { - callback.mfaSetup('challengeName', 'challengeParam'); + callback.mfaSetup('MFA_SETUP', 'challengeParam'); }); const auth = new Auth(authOptions); const user = new CognitoUser({ @@ -1043,7 +1175,7 @@ describe('auth unit test', () => { Pool: userPool, }); const userAfterSignedIn = Object.assign({}, user, { - challengeName: 'challengeName', + challengeName: 'MFA_SETUP', challengeParam: 'challengeParam', }); @@ -1059,7 +1191,7 @@ describe('auth unit test', () => { const spyon = jest .spyOn(CognitoUser.prototype, 'authenticateUser') .mockImplementationOnce((authenticationDetails, callback) => { - callback.totpRequired('challengeName', 'challengeParam'); + callback.totpRequired('SOFTWARE_TOKEN_MFA', 'challengeParam'); }); const auth = new Auth(authOptions); const user = new CognitoUser({ @@ -1067,7 +1199,7 @@ describe('auth unit test', () => { Pool: userPool, }); const userAfterSignedIn = Object.assign({}, user, { - challengeName: 'challengeName', + challengeName: 'SOFTWARE_TOKEN_MFA', challengeParam: 'challengeParam', }); @@ -1083,7 +1215,7 @@ describe('auth unit test', () => { const spyon = jest .spyOn(CognitoUser.prototype, 'authenticateUser') .mockImplementationOnce((authenticationDetails, callback) => { - callback.selectMFAType('challengeName', 'challengeParam'); + callback.selectMFAType('SELECT_MFA_TYPE', 'challengeParam'); }); const auth = new Auth(authOptions); const user = new CognitoUser({ @@ -1091,7 +1223,7 @@ describe('auth unit test', () => { Pool: userPool, }); const userAfterSignedIn = Object.assign({}, user, { - challengeName: 'challengeName', + challengeName: 'SELECT_MFA_TYPE', challengeParam: 'challengeParam', }); @@ -1403,7 +1535,7 @@ describe('auth unit test', () => { const spyon = jest .spyOn(CognitoUser.prototype, 'completeNewPasswordChallenge') .mockImplementationOnce((password, requiredAttributes, callback) => { - callback.mfaRequired('challengeName', 'challengeParam'); + callback.mfaRequired('SMS_MFA', 'challengeParam'); }); const auth = new Auth(authOptions); @@ -1422,7 +1554,7 @@ describe('auth unit test', () => { const spyon = jest .spyOn(CognitoUser.prototype, 'completeNewPasswordChallenge') .mockImplementationOnce((password, requiredAttributes, callback) => { - callback.mfaSetup('challengeName', 'challengeParam'); + callback.mfaSetup('MFA_SETUP', 'challengeParam'); }); const auth = new Auth(authOptions); @@ -1517,6 +1649,7 @@ describe('auth unit test', () => { describe('currentSession', () => { afterEach(() => { jest.clearAllMocks(); + jest.useRealTimers(); }); test('happy case', async () => { const auth = new Auth(authOptions); @@ -1603,13 +1736,12 @@ describe('auth unit test', () => { identityPoolId: 'awsCognitoIdentityPoolId', mandatorySignIn: false, }); - const errorMessage = new NoUserPoolError( - AuthErrorTypes.MissingAuthConfig - ); + + const noUserPoolError = Error('No User Pool in the configuration.'); expect.assertions(2); - expect(auth.currentSession().then()).rejects.toThrow(NoUserPoolError); - expect(auth.currentSession().then()).rejects.toEqual(errorMessage); + expect(auth.currentSession().then()).rejects.toThrow(Error); + expect(auth.currentSession().then()).rejects.toEqual(noUserPoolError); }); }); diff --git a/packages/auth/__tests__/hosted-ui.test.ts b/packages/auth/__tests__/hosted-ui.test.ts index 0c995c2c05a..57df945a38c 100644 --- a/packages/auth/__tests__/hosted-ui.test.ts +++ b/packages/auth/__tests__/hosted-ui.test.ts @@ -157,7 +157,8 @@ jest.mock('amazon-cognito-identity-js/lib/CognitoUser', () => { return CognitoUser; }); -import { Hub, Credentials, StorageHelper, JS } from '@aws-amplify/core'; +import * as AmplifyCore from '@aws-amplify/core'; +const { Hub, Credentials, StorageHelper } = AmplifyCore; const authOptionsWithOAuth: AuthOptions = { userPoolId: 'awsUserPoolsId', @@ -263,7 +264,7 @@ describe('Hosted UI tests', () => { }; }); - jest.spyOn(JS, 'browserOrNode').mockImplementation(() => ({ + jest.spyOn(AmplifyCore, 'browserOrNode').mockImplementation(() => ({ isBrowser: true, isNode: false, })); @@ -317,7 +318,7 @@ describe('Hosted UI tests', () => { }; }); - jest.spyOn(JS, 'browserOrNode').mockImplementation(() => ({ + jest.spyOn(AmplifyCore, 'browserOrNode').mockImplementation(() => ({ isBrowser: false, isNode: true, })); @@ -371,7 +372,7 @@ describe('Hosted UI tests', () => { }; }); - jest.spyOn(JS, 'browserOrNode').mockImplementation(() => ({ + jest.spyOn(AmplifyCore, 'browserOrNode').mockImplementation(() => ({ isBrowser: false, isNode: true, })); @@ -417,7 +418,7 @@ describe('Hosted UI tests', () => { }; }); - jest.spyOn(JS, 'browserOrNode').mockImplementation(() => ({ + jest.spyOn(AmplifyCore, 'browserOrNode').mockImplementation(() => ({ isBrowser: true, isNode: false, })); diff --git a/packages/auth/package.json b/packages/auth/package.json index 613d0f6a8e1..95daca549d4 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/auth", - "version": "4.5.2", + "version": "4.6.4", "description": "Auth category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -41,9 +41,9 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/cache": "4.0.40", - "@aws-amplify/core": "4.5.2", - "amazon-cognito-identity-js": "5.2.8", + "@aws-amplify/cache": "4.0.53", + "@aws-amplify/core": "4.7.2", + "amazon-cognito-identity-js": "5.2.10", "crypto-js": "^4.1.1" }, "devDependencies": { diff --git a/packages/auth/src/Auth.ts b/packages/auth/src/Auth.ts index ca9d71a6e39..a52cf9666a4 100644 --- a/packages/auth/src/Auth.ts +++ b/packages/auth/src/Auth.ts @@ -41,9 +41,10 @@ import { StorageHelper, ICredentials, Parser, - JS, + browserOrNode, UniversalStorage, urlSafeDecode, + HubCallback, } from '@aws-amplify/core'; import { CookieStorage, @@ -70,6 +71,7 @@ import { default as urlListener } from './urlListener'; import { AuthError, NoUserPoolError } from './Errors'; import { AuthErrorTypes, + AutoSignInOptions, CognitoHostedUIIdentityProvider, IAuthDevice, } from './types/Auth'; @@ -95,6 +97,8 @@ const dispatchAuthEvent = (event: string, data: any, message: string) => { // https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListDevices.html#API_ListDevices_RequestSyntax const MAX_DEVICES = 60; +const MAX_AUTOSIGNIN_POLLING_MS = 3 * 60 * 1000; + /** * Provide authentication steps */ @@ -107,6 +111,7 @@ export class AuthClass { private _storageSync; private oAuthFlowInProgress: boolean = false; private pendingSignIn: ReturnType | null; + private autoSignInInitiated: boolean = false; Credentials = Credentials; @@ -257,6 +262,24 @@ export class AuthClass { null, `The Auth category has been configured successfully` ); + + if ( + !this.autoSignInInitiated && + typeof this._storage['getItem'] === 'function' + ) { + const pollingInitiated = this.isTrueStorageValue( + 'amplify-polling-started' + ); + if (pollingInitiated) { + dispatchAuthEvent( + 'autoSignIn_failure', + null, + AuthErrorTypes.AutoSignInError + ); + this._storage.removeItem('amplify-auto-sign-in'); + } + this._storage.removeItem('amplify-polling-started'); + } return this._config; } @@ -295,6 +318,9 @@ export class AuthClass { const attributes: CognitoUserAttribute[] = []; let validationData: CognitoUserAttribute[] = null; let clientMetadata; + let autoSignIn: AutoSignInOptions = { enabled: false }; + let autoSignInValidationData = {}; + let autoSignInClientMetaData: ClientMetaData = {}; if (params && typeof params === 'string') { username = params; @@ -345,6 +371,13 @@ export class AuthClass { ); }); } + + autoSignIn = params.autoSignIn ?? { enabled: false }; + if (autoSignIn.enabled) { + this._storage.setItem('amplify-auto-sign-in', 'true'); + autoSignInValidationData = autoSignIn.validationData ?? {}; + autoSignInClientMetaData = autoSignIn.clientMetaData ?? {}; + } } else { return this.rejectAuthError(AuthErrorTypes.SignUpError); } @@ -379,6 +412,15 @@ export class AuthClass { data, `${username} has signed up successfully` ); + if (autoSignIn.enabled) { + this.handleAutoSignIn( + username, + password, + autoSignInValidationData, + autoSignInClientMetaData, + data + ); + } resolve(data); } }, @@ -387,6 +429,97 @@ export class AuthClass { }); } + private handleAutoSignIn( + username: string, + password: string, + validationData: {}, + clientMetadata: any, + data: any + ) { + this.autoSignInInitiated = true; + const authDetails = new AuthenticationDetails({ + Username: username, + Password: password, + ValidationData: validationData, + ClientMetadata: clientMetadata, + }); + if (data.userConfirmed) { + this.signInAfterUserConfirmed(authDetails); + } else if (this._config.signUpVerificationMethod === 'link') { + this.handleLinkAutoSignIn(authDetails); + } else { + this.handleCodeAutoSignIn(authDetails); + } + } + + private handleCodeAutoSignIn(authDetails: AuthenticationDetails) { + const listenEvent = ({ payload }) => { + if (payload.event === 'confirmSignUp') { + this.signInAfterUserConfirmed(authDetails, listenEvent); + } + }; + Hub.listen('auth', listenEvent); + } + + private handleLinkAutoSignIn(authDetails: AuthenticationDetails) { + this._storage.setItem('amplify-polling-started', 'true'); + const start = Date.now(); + const autoSignInPollingIntervalId = setInterval(() => { + if (Date.now() - start > MAX_AUTOSIGNIN_POLLING_MS) { + clearInterval(autoSignInPollingIntervalId); + dispatchAuthEvent( + 'autoSignIn_failure', + null, + 'Please confirm your account and use your credentials to sign in.' + ); + this._storage.removeItem('amplify-auto-sign-in'); + } else { + this.signInAfterUserConfirmed( + authDetails, + null, + autoSignInPollingIntervalId + ); + } + }, 5000); + } + + private async signInAfterUserConfirmed( + authDetails: AuthenticationDetails, + listenEvent?: HubCallback, + autoSignInPollingIntervalId?: ReturnType + ) { + const user = this.createCognitoUser(authDetails.getUsername()); + try { + await user.authenticateUser( + authDetails, + this.authCallbacks( + user, + value => { + dispatchAuthEvent( + 'autoSignIn', + value, + `${authDetails.getUsername()} has signed in successfully` + ); + if (listenEvent) { + Hub.remove('auth', listenEvent); + } + if (autoSignInPollingIntervalId) { + clearInterval(autoSignInPollingIntervalId); + this._storage.removeItem('amplify-polling-started'); + } + this._storage.removeItem('amplify-auto-sign-in'); + }, + error => { + logger.error(error); + this._storage.removeItem('amplify-auto-sign-in'); + } + ) + ); + } catch (error) { + logger.error(error); + } + } + /** * Send the verification code to confirm sign up * @param {String} username - The username to be confirmed @@ -429,6 +562,20 @@ export class AuthClass { if (err) { reject(err); } else { + dispatchAuthEvent( + 'confirmSignUp', + data, + `${username} has been confirmed successfully` + ); + const autoSignIn = this.isTrueStorageValue('amplify-auto-sign-in'); + if (autoSignIn && !this.autoSignInInitiated) { + dispatchAuthEvent( + 'autoSignIn_failure', + null, + AuthErrorTypes.AutoSignInError + ); + this._storage.removeItem('amplify-auto-sign-in'); + } resolve(data); } }, @@ -437,6 +584,11 @@ export class AuthClass { }); } + private isTrueStorageValue(value: string) { + const item = this._storage.getItem(value); + return item ? item === 'true' : false; + } + /** * Resend the verification code * @param {String} username - The username to be confirmed @@ -1636,7 +1788,7 @@ export class AuthClass { logger.debug('Getting current session'); // Purposely not calling the reject method here because we don't need a console error if (!this.userPool) { - return this.rejectNoUserPool(); + return Promise.reject(new Error('No User Pool in the configuration.')); } return new Promise((res, rej) => { @@ -1905,7 +2057,7 @@ export class AuthClass { resolve: () => void, reject: (reason?: any) => void ) { - const { isBrowser } = JS.browserOrNode(); + const { isBrowser } = browserOrNode(); if (isBrowser) { this.oAuthSignOutRedirectOrReject(reject); @@ -2273,7 +2425,7 @@ export class AuthClass { ); const currentUrl = - URL || (JS.browserOrNode().isBrowser ? window.location.href : ''); + URL || (browserOrNode().isBrowser ? window.location.href : ''); const hasCodeOrError = !!(parse(currentUrl).query || '') .split('&') diff --git a/packages/auth/src/Errors.ts b/packages/auth/src/Errors.ts index 2fc7c79d1b8..dcc34d1d886 100644 --- a/packages/auth/src/Errors.ts +++ b/packages/auth/src/Errors.ts @@ -110,6 +110,9 @@ export const authErrorMessages: AuthErrorMessages = { networkError: { message: AuthErrorStrings.NETWORK_ERROR, }, + autoSignInError: { + message: AuthErrorStrings.AUTOSIGNIN_ERROR, + }, default: { message: AuthErrorStrings.DEFAULT_MSG, }, diff --git a/packages/auth/src/common/AuthErrorStrings.ts b/packages/auth/src/common/AuthErrorStrings.ts index 249a4927610..ae6e20d184d 100644 --- a/packages/auth/src/common/AuthErrorStrings.ts +++ b/packages/auth/src/common/AuthErrorStrings.ts @@ -13,4 +13,5 @@ export enum AuthErrorStrings { NO_USER_SESSION = 'Failed to get the session because the user is empty', NETWORK_ERROR = 'Network Error', DEVICE_CONFIG = 'Device tracking has not been configured in this User Pool', + AUTOSIGNIN_ERROR = 'Please use your credentials to sign in', } diff --git a/packages/auth/src/types/Auth.ts b/packages/auth/src/types/Auth.ts index 17d0d440852..cbc668a1aa2 100644 --- a/packages/auth/src/types/Auth.ts +++ b/packages/auth/src/types/Auth.ts @@ -25,6 +25,7 @@ export interface SignUpParams { attributes?: object; validationData?: { [key: string]: any }; clientMetadata?: { [key: string]: string }; + autoSignIn?: AutoSignInOptions; } export interface AuthCache { @@ -50,6 +51,7 @@ export interface AuthOptions { identityPoolRegion?: string; clientMetadata?: any; endpoint?: string; + signUpVerificationMethod?: 'code' | 'link'; } export enum CognitoHostedUIIdentityProvider { @@ -201,6 +203,7 @@ export enum AuthErrorTypes { Default = 'default', DeviceConfig = 'deviceConfig', NetworkError = 'networkError', + AutoSignInError = 'autoSignInError', } export type AuthErrorMessages = { [key in AuthErrorTypes]: AuthErrorMessage }; @@ -228,6 +231,12 @@ export interface IAuthDevice { name: string; } +export interface AutoSignInOptions { + enabled: boolean; + clientMetaData?: ClientMetaData; + validationData?: { [key: string]: any }; +} + export enum GRAPHQL_AUTH_MODE { API_KEY = 'API_KEY', AWS_IAM = 'AWS_IAM', diff --git a/packages/auth/src/urlListener.ts b/packages/auth/src/urlListener.ts index 2857e997d00..cad8429e2ce 100644 --- a/packages/auth/src/urlListener.ts +++ b/packages/auth/src/urlListener.ts @@ -10,14 +10,14 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ -import { JS } from '@aws-amplify/core'; +import { browserOrNode } from '@aws-amplify/core'; export default callback => { - if (JS.browserOrNode().isBrowser && window.location) { + if (browserOrNode().isBrowser && window.location) { const url = window.location.href; callback({ url }); - } else if (JS.browserOrNode().isNode) { + } else if (browserOrNode().isNode) { // continue building on ssr () => {}; // noop } else { diff --git a/packages/aws-amplify-angular/CHANGELOG.md b/packages/aws-amplify-angular/CHANGELOG.md index 54646bbfac5..8c3d53a8daf 100644 --- a/packages/aws-amplify-angular/CHANGELOG.md +++ b/packages/aws-amplify-angular/CHANGELOG.md @@ -3,6 +3,113 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [6.0.51](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.50...aws-amplify-angular@6.0.51) (2022-08-23) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.50](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.49...aws-amplify-angular@6.0.50) (2022-08-18) + + +### Bug Fixes + +* An update to @types/lodash breaks the build - specify last working version to unblock ([#10221](https://github.com/aws-amplify/amplify-js/issues/10221)) ([f54b645](https://github.com/aws-amplify/amplify-js/commit/f54b64502bb163307a1e8701d686d9b6b90a6eee)) + + + + + +## [6.0.49](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.48...aws-amplify-angular@6.0.49) (2022-08-16) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.48](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.47...aws-amplify-angular@6.0.48) (2022-08-01) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.47](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.46...aws-amplify-angular@6.0.47) (2022-07-28) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.46](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.45...aws-amplify-angular@6.0.46) (2022-07-21) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.45](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.44...aws-amplify-angular@6.0.45) (2022-07-07) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.44](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.43...aws-amplify-angular@6.0.44) (2022-06-18) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.43](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.42...aws-amplify-angular@6.0.43) (2022-06-15) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.42](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.41...aws-amplify-angular@6.0.42) (2022-05-24) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.41](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.40...aws-amplify-angular@6.0.41) (2022-05-23) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.40](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.39...aws-amplify-angular@6.0.40) (2022-05-12) + +**Note:** Version bump only for package aws-amplify-angular + + + + + +## [6.0.39](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.38...aws-amplify-angular@6.0.39) (2022-05-03) + +**Note:** Version bump only for package aws-amplify-angular + + + + + ## [6.0.38](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-angular@6.0.37...aws-amplify-angular@6.0.38) (2022-04-14) **Note:** Version bump only for package aws-amplify-angular diff --git a/packages/aws-amplify-angular/package.json b/packages/aws-amplify-angular/package.json index 1eae4bf9e37..f56da3a5cb7 100644 --- a/packages/aws-amplify-angular/package.json +++ b/packages/aws-amplify-angular/package.json @@ -1,7 +1,7 @@ { "name": "aws-amplify-angular", "private": "true", - "version": "6.0.38", + "version": "6.0.51", "description": "AWS Amplify Angular Components", "main": "bundles/aws-amplify-angular.umd.js", "module": "dist/index.js", @@ -34,13 +34,12 @@ "@angular/forms": "^6.0.3", "@angular/platform-browser": "^5.2.9", "@angular/platform-browser-dynamic": "^5.2.9", - "@types/lodash": "^4.14.106", "@types/node": "^9.4.6", "@types/paho-mqtt": "^1.0.3", "@types/zen-observable": "^0.5.3", "angular2-template-loader": "^0.6.2", "awesome-typescript-loader": "^4.0.1", - "aws-amplify": "4.3.20", + "aws-amplify": "4.3.33", "babel-core": "^6.26.3", "babel-plugin-lodash": "^3.3.4", "babel-preset-env": "^1.7.0", diff --git a/packages/aws-amplify-react-native/CHANGELOG.md b/packages/aws-amplify-react-native/CHANGELOG.md index 2b420c0410a..96fa975cd60 100644 --- a/packages/aws-amplify-react-native/CHANGELOG.md +++ b/packages/aws-amplify-react-native/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [6.0.5](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react-native@6.0.4...aws-amplify-react-native@6.0.5) (2022-06-15) + + +### Bug Fixes + +* **aws-amplify-react-native:** set Resend Code enabled/disabled from current username value ([#9767](https://github.com/aws-amplify/amplify-js/issues/9767)) ([94813a9](https://github.com/aws-amplify/amplify-js/commit/94813a9b364f9d13c72da38c0c13aefbe52157d7)) + + + + + ## [6.0.4](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react-native@6.0.3...aws-amplify-react-native@6.0.4) (2022-03-28) diff --git a/packages/aws-amplify-react-native/package.json b/packages/aws-amplify-react-native/package.json index 2a98fed0467..e272f921836 100644 --- a/packages/aws-amplify-react-native/package.json +++ b/packages/aws-amplify-react-native/package.json @@ -1,6 +1,6 @@ { "name": "aws-amplify-react-native", - "version": "6.0.4", + "version": "6.0.5", "description": "AWS Amplify is a JavaScript library for Frontend and mobile developers building cloud-enabled applications.", "main": "dist/index.js", "scripts": { diff --git a/packages/aws-amplify-react-native/src/Auth/ConfirmSignUp.tsx b/packages/aws-amplify-react-native/src/Auth/ConfirmSignUp.tsx index 424bfff4829..5f25792175b 100644 --- a/packages/aws-amplify-react-native/src/Auth/ConfirmSignUp.tsx +++ b/packages/aws-amplify-react-native/src/Auth/ConfirmSignUp.tsx @@ -14,18 +14,10 @@ import React from 'react'; import { View } from 'react-native'; import { Auth, I18n, Logger } from 'aws-amplify'; -import { - FormField, - LinkCell, - Header, - ErrorRow, - AmplifyButton, - SignedOutMessage, - Wrapper, -} from '../AmplifyUI'; +import { FormField, LinkCell, Header, ErrorRow, AmplifyButton, SignedOutMessage, Wrapper } from '../AmplifyUI'; import AuthPiece, { IAuthPieceProps, IAuthPieceState } from './AuthPiece'; import TEST_ID from '../AmplifyTestIDs'; -import { setTestId } from '../Utils' +import { setTestId } from '../Utils'; const logger = new Logger('ConfirmSignUp'); @@ -35,10 +27,7 @@ interface IConfirmSignUpState extends IAuthPieceState { code: string | null; } -export default class ConfirmSignUp extends AuthPiece< - IConfirmSignUpProps, - IConfirmSignUpState -> { +export default class ConfirmSignUp extends AuthPiece { constructor(props: IConfirmSignUpProps) { super(props); @@ -58,8 +47,8 @@ export default class ConfirmSignUp extends AuthPiece< const username = this.getUsernameFromInput(); logger.debug('Confirm Sign Up for ' + username); Auth.confirmSignUp(username, code) - .then(data => this.changeState('signedUp')) - .catch(err => this.error(err)); + .then((data) => this.changeState('signedUp')) + .catch((err) => this.error(err)); } resend() { @@ -67,7 +56,7 @@ export default class ConfirmSignUp extends AuthPiece< logger.debug('Resend Sign Up for ' + username); Auth.resendSignUp(username) .then(() => logger.debug('code sent')) - .catch(err => this.error(err)); + .catch((err) => this.error(err)); } static getDerivedStateFromProps(props, state) { @@ -93,7 +82,7 @@ export default class ConfirmSignUp extends AuthPiece< {this.renderUsernameField(theme)} this.setState({ code: text })} + onChangeText={(text) => this.setState({ code: text })} label={I18n.get('Confirmation Code')} placeholder={I18n.get('Enter your confirmation code')} required={true} @@ -111,7 +100,7 @@ export default class ConfirmSignUp extends AuthPiece< {I18n.get('Resend code')} diff --git a/packages/aws-amplify-react/CHANGELOG.md b/packages/aws-amplify-react/CHANGELOG.md index 52703bff75b..1b295a39c27 100644 --- a/packages/aws-amplify-react/CHANGELOG.md +++ b/packages/aws-amplify-react/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [5.1.34](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.33...aws-amplify-react@5.1.34) (2022-08-23) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.33](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.32...aws-amplify-react@5.1.33) (2022-08-18) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.32](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.31...aws-amplify-react@5.1.32) (2022-08-16) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.31](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.30...aws-amplify-react@5.1.31) (2022-08-01) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.30](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.29...aws-amplify-react@5.1.30) (2022-07-28) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.29](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.28...aws-amplify-react@5.1.29) (2022-07-21) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.28](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.27...aws-amplify-react@5.1.28) (2022-07-07) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.27](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.26...aws-amplify-react@5.1.27) (2022-06-18) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.26](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.25...aws-amplify-react@5.1.26) (2022-06-15) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.25](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.24...aws-amplify-react@5.1.25) (2022-05-24) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.24](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.23...aws-amplify-react@5.1.24) (2022-05-23) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.23](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.22...aws-amplify-react@5.1.23) (2022-05-12) + +**Note:** Version bump only for package aws-amplify-react + + + + + +## [5.1.22](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.21...aws-amplify-react@5.1.22) (2022-05-03) + +**Note:** Version bump only for package aws-amplify-react + + + + + ## [5.1.21](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-react@5.1.20...aws-amplify-react@5.1.21) (2022-04-14) **Note:** Version bump only for package aws-amplify-react diff --git a/packages/aws-amplify-react/package.json b/packages/aws-amplify-react/package.json index c4589cd94f9..4af2c38d55d 100644 --- a/packages/aws-amplify-react/package.json +++ b/packages/aws-amplify-react/package.json @@ -1,7 +1,7 @@ { "name": "aws-amplify-react", "private": "true", - "version": "5.1.21", + "version": "5.1.34", "description": "AWS Amplify is a JavaScript library for Frontend and mobile developers building cloud-enabled applications.", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -39,7 +39,7 @@ "@types/enzyme-adapter-react-16": "^1.0.3", "@types/react": "^16.0.41", "@types/react-dom": "^16.0.11", - "aws-amplify": "4.3.20", + "aws-amplify": "4.3.33", "enzyme": "^3.1.0", "enzyme-adapter-react-16": "^1.0.3", "enzyme-to-json": "^3.2.1", diff --git a/packages/aws-amplify-vue/CHANGELOG.md b/packages/aws-amplify-vue/CHANGELOG.md index 7cd3e6764ab..a78490411e1 100644 --- a/packages/aws-amplify-vue/CHANGELOG.md +++ b/packages/aws-amplify-vue/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.1.7](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-vue@2.1.6...aws-amplify-vue@2.1.7) (2022-07-07) + + +### Bug Fixes + +* pin vue version ([#10052](https://github.com/aws-amplify/amplify-js/issues/10052)) ([870ec87](https://github.com/aws-amplify/amplify-js/commit/870ec87a6b5d6f3aa3a85551faac87a208c354a2)) + + + + + ## [2.1.6](https://github.com/aws-amplify/amplify-js/compare/aws-amplify-vue@2.1.5...aws-amplify-vue@2.1.6) (2021-12-02) **Note:** Version bump only for package aws-amplify-vue diff --git a/packages/aws-amplify-vue/package.json b/packages/aws-amplify-vue/package.json index 088c2e173f3..c2d86fe3aa5 100644 --- a/packages/aws-amplify-vue/package.json +++ b/packages/aws-amplify-vue/package.json @@ -1,7 +1,7 @@ { "name": "aws-amplify-vue", "private": "true", - "version": "2.1.6", + "version": "2.1.7", "license": "Apache-2.0", "author": "Amazon Web Services", "scripts": { @@ -15,7 +15,7 @@ "dependencies": { "lodash.orderby": "^4.6.0", "qrcode.vue": "^1.6.0", - "vue": "^2.5.17", + "vue": "2.6.11", "vue2-filters": "^0.7.2" }, "devDependencies": { @@ -26,7 +26,7 @@ "@vue/test-utils": "1.0.0-beta.29", "babel-core": "7.0.0-bridge.0", "postcss-loader": "^2.1.6", - "vue-template-compiler": "^2.5.17" + "vue-template-compiler": "2.6.11" }, "postcss": { "plugins": { diff --git a/packages/aws-amplify/CHANGELOG.md b/packages/aws-amplify/CHANGELOG.md index 96d74ba657a..00885200f93 100644 --- a/packages/aws-amplify/CHANGELOG.md +++ b/packages/aws-amplify/CHANGELOG.md @@ -3,6 +3,113 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.3.33](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.32...aws-amplify@4.3.33) (2022-08-23) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.32](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.31...aws-amplify@4.3.32) (2022-08-18) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.31](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.30...aws-amplify@4.3.31) (2022-08-16) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.30](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.29...aws-amplify@4.3.30) (2022-08-01) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.29](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.28...aws-amplify@4.3.29) (2022-07-28) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.28](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.27...aws-amplify@4.3.28) (2022-07-21) + + +### Bug Fixes + +* preserve ssr context when using DataStore ([#10088](https://github.com/aws-amplify/amplify-js/issues/10088)) ([a10d920](https://github.com/aws-amplify/amplify-js/commit/a10d920f7fb6199539fb8d9cec2cb4426dbfd47b)) + + + + + +## [4.3.27](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.26...aws-amplify@4.3.27) (2022-07-07) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.26](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.25...aws-amplify@4.3.26) (2022-06-18) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.25](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.24...aws-amplify@4.3.25) (2022-06-15) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.24](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.23...aws-amplify@4.3.24) (2022-05-24) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.23](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.22...aws-amplify@4.3.23) (2022-05-23) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.22](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.21...aws-amplify@4.3.22) (2022-05-12) + +**Note:** Version bump only for package aws-amplify + + + + + +## [4.3.21](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.20...aws-amplify@4.3.21) (2022-05-03) + +**Note:** Version bump only for package aws-amplify + + + + + ## [4.3.20](https://github.com/aws-amplify/amplify-js/compare/aws-amplify@4.3.19...aws-amplify@4.3.20) (2022-04-14) **Note:** Version bump only for package aws-amplify diff --git a/packages/aws-amplify/__tests__/withSSRContext-test.ts b/packages/aws-amplify/__tests__/withSSRContext-test.ts index 6e487f8757f..e1ad9c5dc53 100644 --- a/packages/aws-amplify/__tests__/withSSRContext-test.ts +++ b/packages/aws-amplify/__tests__/withSSRContext-test.ts @@ -1,4 +1,4 @@ -import { Amplify, CredentialsClass, UniversalStorage } from '@aws-amplify/core'; +import { Amplify, UniversalStorage } from '@aws-amplify/core'; import { withSSRContext } from '../src/withSSRContext'; @@ -69,6 +69,16 @@ describe('withSSRContext', () => { it('should be a different instance than Amplify.DataStore', () => { expect(withSSRContext().DataStore).not.toBe(Amplify.DataStore); }); + + it('should use Amplify components from the ssr context', () => { + const { Auth, API, DataStore } = withSSRContext(); + + expect(DataStore.Auth).toBe(Auth); + expect(DataStore.Auth).not.toBe(Amplify.Auth); + + expect(DataStore.API).toBe(API); + expect(DataStore.API).not.toBe(Amplify.API); + }); }); describe('I18n', () => { diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 3579a332cad..a5be88832cf 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -1,6 +1,6 @@ { "name": "aws-amplify", - "version": "4.3.20", + "version": "4.3.33", "description": "AWS Amplify is a JavaScript library for Frontend and mobile developers building cloud-enabled applications.", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -34,19 +34,19 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/analytics": "5.2.5", - "@aws-amplify/api": "4.0.38", - "@aws-amplify/auth": "4.5.2", - "@aws-amplify/cache": "4.0.40", - "@aws-amplify/core": "4.5.2", - "@aws-amplify/datastore": "3.10.0", - "@aws-amplify/geo": "1.3.1", - "@aws-amplify/interactions": "4.0.38", - "@aws-amplify/predictions": "4.0.38", - "@aws-amplify/pubsub": "4.3.2", - "@aws-amplify/storage": "4.4.21", + "@aws-amplify/analytics": "5.2.18", + "@aws-amplify/api": "4.0.51", + "@aws-amplify/auth": "4.6.4", + "@aws-amplify/cache": "4.0.53", + "@aws-amplify/core": "4.7.2", + "@aws-amplify/datastore": "3.12.8", + "@aws-amplify/geo": "1.3.14", + "@aws-amplify/interactions": "4.0.51", + "@aws-amplify/predictions": "4.0.51", + "@aws-amplify/pubsub": "4.5.1", + "@aws-amplify/storage": "4.5.4", "@aws-amplify/ui": "2.0.5", - "@aws-amplify/xr": "3.0.38" + "@aws-amplify/xr": "3.0.51" }, "jest": { "globals": { diff --git a/packages/cache/CHANGELOG.md b/packages/cache/CHANGELOG.md index 7df476620ab..eb4190e7206 100644 --- a/packages/cache/CHANGELOG.md +++ b/packages/cache/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.53](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.52...@aws-amplify/cache@4.0.53) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.52](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.51...@aws-amplify/cache@4.0.52) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.50...@aws-amplify/cache@4.0.51) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.49...@aws-amplify/cache@4.0.50) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.48...@aws-amplify/cache@4.0.49) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.47...@aws-amplify/cache@4.0.48) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.46...@aws-amplify/cache@4.0.47) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.45...@aws-amplify/cache@4.0.46) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.44...@aws-amplify/cache@4.0.45) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.43...@aws-amplify/cache@4.0.44) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.42...@aws-amplify/cache@4.0.43) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.41...@aws-amplify/cache@4.0.42) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + +## [4.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.40...@aws-amplify/cache@4.0.41) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/cache + + + + + ## [4.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.39...@aws-amplify/cache@4.0.40) (2022-04-14) **Note:** Version bump only for package @aws-amplify/cache diff --git a/packages/cache/package.json b/packages/cache/package.json index e5a3142e670..2c684ad618f 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/cache", - "version": "4.0.40", + "version": "4.0.53", "description": "Cache category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -44,7 +44,7 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/core": "4.5.2" + "@aws-amplify/core": "4.7.2" }, "jest": { "globals": { diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index ec7f0ceecee..6c8af693831 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -3,6 +3,116 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.7.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.7.1...@aws-amplify/core@4.7.2) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.7.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.7.0...@aws-amplify/core@4.7.1) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +# [4.7.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.6.1...@aws-amplify/core@4.7.0) (2022-08-16) + + +### Features + +* **@aws-amplify/core:** Throw Error if body attribute passed to Sign… ([#10137](https://github.com/aws-amplify/amplify-js/issues/10137)) ([360bde2](https://github.com/aws-amplify/amplify-js/commit/360bde20716778b69af339f4f66b42c05ccf4639)) + + + + + +## [4.6.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.6.0...@aws-amplify/core@4.6.1) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +# [4.6.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.10...@aws-amplify/core@4.6.0) (2022-07-28) + + +### Features + +* **@aws-amplify/auth:** Auto sign in after sign up ([#10126](https://github.com/aws-amplify/amplify-js/issues/10126)) ([e54617f](https://github.com/aws-amplify/amplify-js/commit/e54617f2878244f0e391d2d49f5cd2e8a8c069f9)), closes [#6320](https://github.com/aws-amplify/amplify-js/issues/6320) [#3882](https://github.com/aws-amplify/amplify-js/issues/3882) [#3631](https://github.com/aws-amplify/amplify-js/issues/3631) [#6018](https://github.com/aws-amplify/amplify-js/issues/6018) + + + + + +## [4.5.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.9...@aws-amplify/core@4.5.10) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.5.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.8...@aws-amplify/core@4.5.9) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.5.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.7...@aws-amplify/core@4.5.8) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.5.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.6...@aws-amplify/core@4.5.7) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.5.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.5...@aws-amplify/core@4.5.6) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.5.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.4...@aws-amplify/core@4.5.5) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.5.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.3...@aws-amplify/core@4.5.4) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/core + + + + + +## [4.5.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.2...@aws-amplify/core@4.5.3) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/core + + + + + ## [4.5.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/core@4.5.1...@aws-amplify/core@4.5.2) (2022-04-14) diff --git a/packages/core/__tests__/Platform-test.ts b/packages/core/__tests__/Platform-test.ts index fd4333073dc..ef5ece74355 100644 --- a/packages/core/__tests__/Platform-test.ts +++ b/packages/core/__tests__/Platform-test.ts @@ -1,3 +1,4 @@ +import { getAmplifyUserAgent } from '../src/Platform'; import Platform from '../src/Platform'; describe('Platform test', () => { @@ -6,4 +7,16 @@ describe('Platform test', () => { expect(Platform.isReactNative).toBe(false); }); }); + + describe('getAmplifyUserAgent test', () => { + test('without content', () => { + expect(getAmplifyUserAgent()).toBe(Platform.userAgent); + }); + + test('with content', () => { + expect(getAmplifyUserAgent('/DataStore')).toBe( + `${Platform.userAgent}/DataStore` + ); + }); + }); }); diff --git a/packages/core/__tests__/Signer-test.ts b/packages/core/__tests__/Signer-test.ts index 0e08de582d1..7440efa0982 100644 --- a/packages/core/__tests__/Signer-test.ts +++ b/packages/core/__tests__/Signer-test.ts @@ -73,3 +73,60 @@ describe('Signer test', () => { }); }); }); +describe('Sign method error', () => { + test('Should throw an Error if body attribute is passed to sign method', () => { + const url = 'https://host/some/path'; + + const request_body = { + url, + headers: {}, + body: {}, + }; + + const access_info = { + session_token: 'session_token', + }; + + const spyon = jest + .spyOn(Date.prototype, 'toISOString') + .mockReturnValueOnce('0'); + + expect(() => { + Signer.sign(request_body, access_info, { + service: 'aservice', + region: 'aregion', + }); + }).toThrowError( + 'The attribute "body" was found on the request object. Please use the attribute "data" instead.' + ); + + spyon.mockClear(); + }); + + test('Should NOT throw an Error if data attribute is passed to sign method', () => { + const url = 'https://host/some/path'; + + const request_data = { + url, + headers: {}, + data: {}, + }; + + const access_info = { + session_token: 'session_token', + }; + + const spyon = jest + .spyOn(Date.prototype, 'toISOString') + .mockReturnValueOnce('0'); + + expect(() => { + Signer.sign(request_data, access_info, { + service: 'aservice', + region: 'aregion', + }); + }).not.toThrowError(); + + spyon.mockClear(); + }); +}); diff --git a/packages/core/__tests__/parseMobileHubConfig-test.ts b/packages/core/__tests__/parseMobileHubConfig-test.ts index 9df261614be..329ffca688c 100644 --- a/packages/core/__tests__/parseMobileHubConfig-test.ts +++ b/packages/core/__tests__/parseMobileHubConfig-test.ts @@ -46,6 +46,7 @@ describe('Parser', () => { region: '', userPoolId: 'b', userPoolWebClientId: '', + signUpVerificationMethod: 'code', }, Geo: { AmazonLocationService: { diff --git a/packages/core/package.json b/packages/core/package.json index a95158171a3..8aec2b4c952 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/core", - "version": "4.5.2", + "version": "4.7.2", "description": "Core category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts index 91328d9e64f..a7e3c0683fc 100644 --- a/packages/core/src/Parser.ts +++ b/packages/core/src/Parser.ts @@ -25,6 +25,8 @@ export const parseMobileHubConfig = (config): AmplifyConfig => { identityPoolId: config['aws_cognito_identity_pool_id'], identityPoolRegion: config['aws_cognito_region'], mandatorySignIn: config['aws_mandatory_sign_in'] === 'enable', + signUpVerificationMethod: + config['aws_cognito_sign_up_verification_method'] || 'code', }; } diff --git a/packages/core/src/Platform/index.ts b/packages/core/src/Platform/index.ts index 1b61ddf98df..8d647810593 100644 --- a/packages/core/src/Platform/index.ts +++ b/packages/core/src/Platform/index.ts @@ -35,8 +35,8 @@ if (typeof navigator !== 'undefined' && navigator.product) { } } -export const getAmplifyUserAgent = () => { - return Platform.userAgent; +export const getAmplifyUserAgent = (content?: string) => { + return `${Platform.userAgent}${content ? content : ''}`; }; /** diff --git a/packages/core/src/Platform/version.ts b/packages/core/src/Platform/version.ts index 44a8e6e212e..0fee46cb61e 100644 --- a/packages/core/src/Platform/version.ts +++ b/packages/core/src/Platform/version.ts @@ -1,2 +1,2 @@ // generated by genversion -export const version = '4.5.2'; +export const version = '4.7.2'; diff --git a/packages/core/src/Signer.ts b/packages/core/src/Signer.ts index 0649a48961e..6475c7eb51e 100644 --- a/packages/core/src/Signer.ts +++ b/packages/core/src/Signer.ts @@ -41,13 +41,7 @@ const hash = function(src) { */ const escape_RFC3986 = function(component) { return component.replace(/[!'()*]/g, function(c) { - return ( - '%' + - c - .charCodeAt(0) - .toString(16) - .toUpperCase() - ); + return '%' + c.charCodeAt(0).toString(16).toUpperCase(); }); }; @@ -290,6 +284,12 @@ export class Signer { static sign(request, access_info, service_info = null) { request.headers = request.headers || {}; + if (request.body && !request.data) { + throw new Error( + 'The attribute "body" was found on the request object. Please use the attribute "data" instead.' + ); + } + // datetime string and date string const dt = DateUtils.getDateWithClockOffset(), dt_str = dt.toISOString().replace(/[:\-]|\.\d{3}/g, ''), diff --git a/packages/datastore-storage-adapter/CHANGELOG.md b/packages/datastore-storage-adapter/CHANGELOG.md index 3d30a957d5f..a489d22651c 100644 --- a/packages/datastore-storage-adapter/CHANGELOG.md +++ b/packages/datastore-storage-adapter/CHANGELOG.md @@ -3,6 +3,123 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.3.11](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.10...@aws-amplify/datastore-storage-adapter@1.3.11) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.9...@aws-amplify/datastore-storage-adapter@1.3.10) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.8...@aws-amplify/datastore-storage-adapter@1.3.9) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.7...@aws-amplify/datastore-storage-adapter@1.3.8) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.6...@aws-amplify/datastore-storage-adapter@1.3.7) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.5...@aws-amplify/datastore-storage-adapter@1.3.6) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.4...@aws-amplify/datastore-storage-adapter@1.3.5) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.3...@aws-amplify/datastore-storage-adapter@1.3.4) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.2...@aws-amplify/datastore-storage-adapter@1.3.3) (2022-06-15) + + +### Bug Fixes + +* Add module declaration files for datastore-storage-adapter ([#9922](https://github.com/aws-amplify/amplify-js/issues/9922)) ([88b6a1e](https://github.com/aws-amplify/amplify-js/commit/88b6a1e82445c359c930ae40a9028ab250870d74)) +* **@aws-amplify/datastore:** adds missing fields to items sent through observe/observeQuery ([#9973](https://github.com/aws-amplify/amplify-js/issues/9973)) ([ca2a11b](https://github.com/aws-amplify/amplify-js/commit/ca2a11b5bc987e71ce3344058a4886bf067cb17b)) + + + + + +## [1.3.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.1...@aws-amplify/datastore-storage-adapter@1.3.2) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/datastore-storage-adapter + + + + + +## [1.3.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.3.0...@aws-amplify/datastore-storage-adapter@1.3.1) (2022-05-23) + + +### Bug Fixes + +* **@aws-amplify/datastore-storage-adapter:** remove extra, invalid sqlite mutations again ([#9921](https://github.com/aws-amplify/amplify-js/issues/9921)) ([00923cf](https://github.com/aws-amplify/amplify-js/commit/00923cfaeafcee97a0f54cc6aa04724f7155e75d)) + + + + + +# [1.3.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.2.13...@aws-amplify/datastore-storage-adapter@1.3.0) (2022-05-12) + + +### Features + +* Added ExpoSQLiteAdapter and Code Sharing for common files ([#9581](https://github.com/aws-amplify/amplify-js/issues/9581)) ([a8ed3c2](https://github.com/aws-amplify/amplify-js/commit/a8ed3c2fad0c780c8782e1729414afd51ff6b155)) + + + + + +## [1.2.13](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.2.12...@aws-amplify/datastore-storage-adapter@1.2.13) (2022-05-03) + + +### Bug Fixes + +* **@aws-amplify/datastore-storage-adapter:** SQLite adapter NULL handling and mutation queue management bugs ([#9813](https://github.com/aws-amplify/amplify-js/issues/9813)) ([fe691fd](https://github.com/aws-amplify/amplify-js/commit/fe691fd4f67adc6ac973dd12ca056563d0720d69)) + + + + + ## [1.2.12](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore-storage-adapter@1.2.11...@aws-amplify/datastore-storage-adapter@1.2.12) (2022-04-14) **Note:** Version bump only for package @aws-amplify/datastore-storage-adapter diff --git a/packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.d.ts b/packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.d.ts new file mode 100644 index 00000000000..9c837eaf844 --- /dev/null +++ b/packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.d.ts @@ -0,0 +1 @@ +declare module '@aws-amplify/datastore-storage-adapter/ExpoSQLiteAdapter'; diff --git a/packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.js b/packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.js new file mode 100644 index 00000000000..a8b71fb330d --- /dev/null +++ b/packages/datastore-storage-adapter/ExpoSQLiteAdapter/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('../dist/aws-amplify-datastore-sqlite-adapter-expo.min.js'); +} else { + module.exports = require('../dist/aws-amplify-datastore-sqlite-adapter-expo.js'); +} diff --git a/packages/datastore-storage-adapter/SQLiteAdapter/index.d.ts b/packages/datastore-storage-adapter/SQLiteAdapter/index.d.ts new file mode 100644 index 00000000000..1546016d174 --- /dev/null +++ b/packages/datastore-storage-adapter/SQLiteAdapter/index.d.ts @@ -0,0 +1 @@ +declare module '@aws-amplify/datastore-storage-adapter/SQLiteAdapter'; diff --git a/packages/datastore-storage-adapter/SQLiteAdapter/index.js b/packages/datastore-storage-adapter/SQLiteAdapter/index.js new file mode 100644 index 00000000000..90611eda4f9 --- /dev/null +++ b/packages/datastore-storage-adapter/SQLiteAdapter/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('../dist/aws-amplify-datastore-storage-adapter.min.js'); +} else { + module.exports = require('../dist/aws-amplify-datastore-storage-adapter.js'); +} diff --git a/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts b/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts index 5f423fee058..7aa6b9eee17 100644 --- a/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts +++ b/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts @@ -1,9 +1,9 @@ import sqlite3 from 'sqlite3'; sqlite3.verbose(); -import { SQLiteAdapter } from '../src'; +import SQLiteAdapter from '../src/SQLiteAdapter/SQLiteAdapter'; import SQLiteDatabase from '../src/SQLiteAdapter/SQLiteDatabase'; -import { ParameterizedStatement } from '../src/SQLiteAdapter/SQLiteUtils'; +import { ParameterizedStatement } from '../src/common/types'; import { DataStore as DataStoreType, StorageAdapter, @@ -13,6 +13,10 @@ import { import { Model, Post, Comment, testSchema } from './helpers'; import { SyncEngine } from '@aws-amplify/datastore/lib/sync'; import Observable from 'zen-observable'; +import { + pause, + addCommonQueryTests, +} from '../../datastore/__tests__/commonAdapterTests'; jest.mock('@aws-amplify/datastore/src/sync/datastoreConnectivity', () => { return { @@ -38,17 +42,6 @@ let initSchema: typeof initSchemaType; let DataStore: typeof DataStoreType; let sqlog: any[]; -/** - * Convenience function to wait for a number of ms. - * - * Intended as a cheap way to wait for async operations to settle. - * - * @param ms number of ms to pause for - */ -async function pause(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - /** * A lower-level SQLite wrapper to test SQLiteAdapter against. * It's intended to be fast, using an in-memory database. @@ -112,29 +105,9 @@ describe('SQLiteAdapter', () => { let Comment: PersistentModelConstructor; let Model: PersistentModelConstructor; let Post: PersistentModelConstructor; - let adapter: StorageAdapter; - let db: SQLiteDatabase; let syncEngine: SyncEngine; sqlog = []; - /** - * Creates the given number of models, with `field1` populated to - * `field1 value ${i}`. - * - * @param qty number of models to create. (default 3) - */ - async function addModels(qty = 3) { - for (let i = 0; i < qty; i++) { - await DataStore.save( - new Model({ - field1: `field1 value ${i}`, - dateCreated: new Date().toISOString(), - emails: [], - }) - ); - } - } - /** * Gets all mutations currently in the outbox. This should include ALL * mutations created/merged, because this test group starts the sync engine, @@ -142,55 +115,102 @@ describe('SQLiteAdapter', () => { */ async function getMutations() { await pause(250); + const adapter = (DataStore as any).storageAdapter; + const db = (adapter as any).db; return await db.getAll('select * from MutationEvent', []); } - beforeEach(async () => { - ({ initSchema, DataStore } = require('@aws-amplify/datastore')); - DataStore.configure({ - storageAdapter: SQLiteAdapter, - }); - (DataStore as any).amplifyConfig.aws_appsync_graphqlEndpoint = - 'https://0.0.0.0/does/not/exist/graphql'; - const classes = initSchema(testSchema()); - ({ Comment, Model, Post } = classes as { - Comment: PersistentModelConstructor; - Model: PersistentModelConstructor; - Post: PersistentModelConstructor; - }); - await DataStore.clear(); + async function clearOutbox() { + await pause(250); + const adapter = (DataStore as any).storageAdapter; + const db = (adapter as any).db; + return await db.executeStatements(['delete from MutationEvent']); + } - // start() ensures storageAdapter is set - await DataStore.start(); + ({ initSchema, DataStore } = require('@aws-amplify/datastore')); + addCommonQueryTests({ + initSchema, + DataStore, + storageAdapter: SQLiteAdapter, + getMutations, + clearOutbox, + }); - adapter = (DataStore as any).storageAdapter; - db = (adapter as any).db; - syncEngine = (DataStore as any).sync; + describe('something', () => { + let adapter: StorageAdapter; + let db: SQLiteDatabase; - // my jest spy-fu wasn't up to snuff here. but, this succesfully - // prevents the mutation process from clearing the mutation queue, which - // allows us to observe the state of mutations. - (syncEngine as any).mutationsProcessor.isReady = () => false; + beforeEach(async () => { + DataStore.configure({ + storageAdapter: SQLiteAdapter, + }); + (DataStore as any).amplifyConfig.aws_appsync_graphqlEndpoint = + 'https://0.0.0.0/does/not/exist/graphql'; + const classes = initSchema(testSchema()); + ({ Comment, Model, Post } = classes as { + Comment: PersistentModelConstructor; + Model: PersistentModelConstructor; + Post: PersistentModelConstructor; + }); + await DataStore.clear(); - sqlog = []; - }); + // start() ensures storageAdapter is set + await DataStore.start(); - describe('sanity checks', () => { - it('is set as the adapter SQLite tests', async () => { - expect(adapter.constructor.name).toEqual('SQLiteAdapter'); - }); + adapter = (DataStore as any).storageAdapter; + db = (adapter as any).db; + syncEngine = (DataStore as any).sync; + + // my jest spy-fu wasn't up to snuff here. but, this succesfully + // prevents the mutation process from clearing the mutation queue, which + // allows us to observe the state of mutations. + (syncEngine as any).mutationsProcessor.isReady = () => false; - it('is logging SQL statements during normal operation', async () => { - // `start()`, which is called during `beforeEach`, should perform - // a number of queries to create tables. the test adapter should - // log these all to `sqlog`. - expect(sqlog.length).toBeGreaterThan(0); + sqlog = []; }); - it('can batchSave', async () => { - const saves = new Set(); - saves.add([ - `insert into "Model" ( + describe('sanity checks', () => { + it('is set as the adapter SQLite tests', async () => { + expect(adapter.constructor.name).toEqual('CommonSQLiteAdapter'); + }); + + it('is logging SQL statements during normal operation', async () => { + // `start()`, which is called during `beforeEach`, should perform + // a number of queries to create tables. the test adapter should + // log these all to `sqlog`. + expect(sqlog.length).toBeGreaterThan(0); + }); + + it('can batchSave', async () => { + const saves = new Set(); + saves.add([ + `insert into "Model" ( + "field1", + "dateCreated", + "emails", + "id", + "_version", + "_lastChangedAt", + "_deleted" + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + 'field1 value 0', + '2022-04-18T19:29:46.316Z', + [], + 'a1d63606-bd3b-4641-870a-ac97694577a8', + null, + null, + null, + ], + ]); + await db.batchSave(saves); + const result = await db.get('select * from "Model" limit 1', []); + expect(result.id).toEqual('a1d63606-bd3b-4641-870a-ac97694577a8'); + }); + + it('can batchQuery', async () => { + await db.save( + `insert into "Model" ( "field1", "dateCreated", "emails", @@ -199,261 +219,26 @@ describe('SQLiteAdapter', () => { "_lastChangedAt", "_deleted" ) VALUES (?, ?, ?, ?, ?, ?, ?)`, - [ - 'field1 value 0', - '2022-04-18T19:29:46.316Z', - [], - 'a1d63606-bd3b-4641-870a-ac97694577a8', - null, - null, - null, - ], - ]); - await db.batchSave(saves); - const result = await db.get('select * from "Model" limit 1', []); - expect(result.id).toEqual('a1d63606-bd3b-4641-870a-ac97694577a8'); - }); - - it('can batchQuery', async () => { - await db.save( - `insert into "Model" ( - "field1", - "dateCreated", - "emails", - "id", - "_version", - "_lastChangedAt", - "_deleted" - ) VALUES (?, ?, ?, ?, ?, ?, ?)`, - [ - 'field1 value 0', - '2022-04-18T19:29:46.316Z', - [], - 'a1d63606-bd3b-4641-870a-ac97694577a8', - null, - null, - null, - ] - ); - - const queries = new Set(); - queries.add([ - 'select * from "Model" where id = ? limit 1', - ['a1d63606-bd3b-4641-870a-ac97694577a8'], - ]); - const result = await db.batchQuery(queries); - - expect(result.length).toBe(1); - }); - }); - - describe('at a high level', () => { - it('can manage a basic model', async () => { - const saved = await DataStore.save( - new Model({ - field1: 'some value', - dateCreated: new Date().toISOString(), - - // why is storage adapter seeing this as a required field? - emails: [], - }) - ); - const retrieved = await DataStore.query(Model, saved.id); - - expect(saved.id).toBeTruthy(); - expect(retrieved).toEqual(saved); - }); - - it('can manage related models, where parent is saved first', async () => { - const post = await DataStore.save( - new Post({ - title: 'some post', - }) - ); - - const comment = await DataStore.save( - new Comment({ - content: 'some comment', - post, - }) - ); - - await DataStore.save( - Comment.copyOf(comment, draft => { - draft.content = 'updated content'; - }) - ); - - const mutations = await getMutations(); - expect(mutations.length).toEqual(2); - }); - - it('should produce a mutation for a nested BELONGS_TO insert', async () => { - await DataStore.save( - new Comment({ - content: 'newly created comment', - post: new Post({ - title: 'newly created post', - }), - }) - ); - - const mutations = await getMutations(); - expect(mutations.length).toEqual(2); - }); - }); - - describe('query', () => { - it('should match fields of any non-empty value for `("ne", undefined)`', async () => { - const qty = 3; - await addModels(qty); - - const results = await DataStore.query(Model, m => - m.field1('ne', undefined) - ); - - expect(results.length).toEqual(qty); - }); - - it('should match fields of any non-empty value for `("ne", null)`', async () => { - const qty = 3; - await addModels(qty); - - const results = await DataStore.query(Model, m => m.field1('ne', null)); - - expect(results.length).toEqual(qty); - }); - - it('should NOT match fields of any non-empty value for `("eq", undefined)`', async () => { - const qty = 3; - await addModels(qty); - - const results = await DataStore.query(Model, m => - m.field1('eq', undefined) - ); - - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("eq", null)`', async () => { - const qty = 3; - await addModels(qty); - - const results = await DataStore.query(Model, m => m.field1('eq', null)); - - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("gt", null)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => m.field1('gt', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("ge", null)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => m.field1('ge', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("lt", null)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => m.field1('lt', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("le", null)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => m.field1('le', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("gt", undefined)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => - m.field1('gt', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("ge", undefined)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => - m.field1('ge', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("lt", undefined)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => - m.field1('lt', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("le", undefined)`', async () => { - const qty = 3; - await addModels(qty); - const results = await DataStore.query(Model, m => - m.field1('le', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should match gt', async () => { - await addModels(3); - const results = await DataStore.query(Model, m => - m.field1('gt', 'field1 value 0') - ); - expect(results.length).toEqual(2); - }); - - it('should match ge', async () => { - await addModels(3); - const results = await DataStore.query(Model, m => - m.field1('ge', 'field1 value 1') - ); - expect(results.length).toEqual(2); - }); - - it('should match lt', async () => { - await addModels(3); - const results = await DataStore.query(Model, m => - m.field1('lt', 'field1 value 2') - ); - expect(results.length).toEqual(2); - }); + [ + 'field1 value 0', + '2022-04-18T19:29:46.316Z', + 'abcd@abcd.com', + 'a1d63606-bd3b-4641-870a-ac97694577a8', + null, + null, + null, + ] + ); - it('should match le', async () => { - await addModels(3); - const results = await DataStore.query(Model, m => - m.field1('le', 'field1 value 1') - ); - expect(results.length).toEqual(2); - }); + const queries = new Set(); + queries.add([ + 'select * from "Model" where id = ? limit 1', + ['a1d63606-bd3b-4641-870a-ac97694577a8'], + ]); + const result = await db.batchQuery(queries); - it('should match eq', async () => { - await addModels(3); - const results = await DataStore.query(Model, m => - m.field1('eq', 'field1 value 1') - ); - expect(results.length).toEqual(1); - }); - - it('should match ne', async () => { - await addModels(3); - const results = await DataStore.query(Model, m => - m.field1('ne', 'field1 value 1') - ); - expect(results.length).toEqual(2); + expect(result.length).toBe(1); + }); }); }); }); diff --git a/packages/datastore-storage-adapter/__tests__/SQLiteUtils.test.ts b/packages/datastore-storage-adapter/__tests__/SQLiteUtils.test.ts index e937c7dce91..a3ae7428878 100644 --- a/packages/datastore-storage-adapter/__tests__/SQLiteUtils.test.ts +++ b/packages/datastore-storage-adapter/__tests__/SQLiteUtils.test.ts @@ -12,7 +12,7 @@ import { deleteByPredicateStatement, modelCreateTableStatement, implicitAuthFieldsForModel, -} from '../src/SQLiteAdapter/SQLiteUtils'; +} from '../src/common/SQLiteUtils'; import { InternalSchema, PersistentModelConstructor, diff --git a/packages/datastore-storage-adapter/__tests__/helpers.ts b/packages/datastore-storage-adapter/__tests__/helpers.ts index 8e2b00a1feb..dd1b4f571f1 100644 --- a/packages/datastore-storage-adapter/__tests__/helpers.ts +++ b/packages/datastore-storage-adapter/__tests__/helpers.ts @@ -3,7 +3,6 @@ import { MutableModel, Schema, InternalSchema, - SchemaModel, } from '@aws-amplify/datastore'; export declare class Model { diff --git a/packages/datastore-storage-adapter/package.json b/packages/datastore-storage-adapter/package.json index a5da3705990..4bc039baa02 100644 --- a/packages/datastore-storage-adapter/package.json +++ b/packages/datastore-storage-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/datastore-storage-adapter", - "version": "1.2.12", + "version": "1.3.11", "description": "SQLite storage adapter for Amplify DataStore ", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -34,8 +34,11 @@ }, "homepage": "https://aws-amplify.github.io/", "devDependencies": { - "@aws-amplify/core": "4.5.2", - "@aws-amplify/datastore": "3.10.0", + "@aws-amplify/core": "4.7.2", + "@aws-amplify/datastore": "3.12.8", + "@types/react-native-sqlite-storage": "5.0.1", + "expo-file-system": "13.1.4", + "expo-sqlite": "10.1.0", "react-native-sqlite-storage": "5.0.0", "sqlite3": "^5.0.2" }, @@ -86,7 +89,8 @@ "/node_modules/", "dist", "lib", - "lib-esm" + "lib-esm", + "../datastore" ] } } diff --git a/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts new file mode 100644 index 00000000000..490563a9d69 --- /dev/null +++ b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts @@ -0,0 +1,8 @@ +import { CommonSQLiteAdapter } from '../common/CommonSQLiteAdapter'; +import ExpoSQLiteDatabase from './ExpoSQLiteDatabase'; + +const ExpoSQLiteAdapter: CommonSQLiteAdapter = new CommonSQLiteAdapter( + new ExpoSQLiteDatabase() +); + +export default ExpoSQLiteAdapter; diff --git a/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts new file mode 100644 index 00000000000..f7e8d5e52ef --- /dev/null +++ b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts @@ -0,0 +1,282 @@ +import { ConsoleLogger as Logger } from '@aws-amplify/core'; +import { PersistentModel } from '@aws-amplify/datastore'; +import { deleteAsync, documentDirectory } from 'expo-file-system'; +import { openDatabase, WebSQLDatabase } from 'expo-sqlite'; +import { DB_NAME } from '../common/constants'; +import { CommonSQLiteDatabase, ParameterizedStatement } from '../common/types'; + +const logger = new Logger('ExpoSQLiteDatabase'); + +/* + +Note: +ExpoSQLite transaction error callbacks require returning a boolean value to indicate whether the +error was handled or not. Returning a true value indicates the error was handled and does not +rollback the whole transaction. + +*/ + +class ExpoSQLiteDatabase implements CommonSQLiteDatabase { + private db: WebSQLDatabase; + + public async init(): Promise { + // only open database once. + + if (!this.db) { + // As per expo docs version, description and size arguments are ignored, + // but are accepted by the function for compatibility with the WebSQL specification. + // Hence, we do not need those arguments. + this.db = openDatabase(DB_NAME); + } + } + + public createSchema(statements: string[]): Promise { + return this.executeStatements(statements); + } + + public async clear(): Promise { + try { + logger.debug('Clearing database'); + await this.closeDB(); + // delete database is not supported by expo-sqlite. + // Database file needs to be deleted using deleteAsync from expo-file-system + await deleteAsync(`${documentDirectory}SQLite/${DB_NAME}`); + logger.debug('Database cleared'); + } catch (error) { + logger.warn('Error clearing the database.', error); + // open database if it was closed earlier and this.db was set to undefined. + this.init(); + } + } + + public async get( + statement: string, + params: (string | number)[] + ): Promise { + const results: T[] = await this.getAll(statement, params); + return results[0]; + } + + public getAll( + statement: string, + params: (string | number)[] + ): Promise { + return new Promise((resolve, reject) => { + this.db.readTransaction(transaction => { + transaction.executeSql( + statement, + params, + (_, result) => { + resolve(result.rows._array || []); + }, + (_, error) => { + reject(error); + logger.warn(error); + return true; + } + ); + }); + }); + } + + public save(statement: string, params: (string | number)[]): Promise { + return new Promise((resolve, reject) => { + this.db.transaction(transaction => { + transaction.executeSql( + statement, + params, + () => { + resolve(null); + }, + (_, error) => { + reject(error); + logger.warn(error); + return true; + } + ); + }); + }); + } + + public batchQuery( + queryParameterizedStatements: Set = new Set() + ): Promise { + return new Promise((resolveTransaction, rejectTransaction) => { + this.db.transaction(async transaction => { + try { + const results: any[] = await Promise.all( + [...queryParameterizedStatements].map( + ([statement, params]) => + new Promise((resolve, reject) => { + transaction.executeSql( + statement, + params, + (_, result) => { + resolve(result.rows._array[0]); + }, + (_, error) => { + reject(error); + logger.warn(error); + return true; + } + ); + }) + ) + ); + resolveTransaction(results); + } catch (error) { + rejectTransaction(error); + logger.warn(error); + } + }); + }); + } + + public batchSave( + saveParameterizedStatements: Set = new Set(), + deleteParameterizedStatements?: Set + ): Promise { + return new Promise((resolveTransaction, rejectTransaction) => { + this.db.transaction(async transaction => { + try { + // await for all sql statements promises to resolve + await Promise.all( + [...saveParameterizedStatements].map( + ([statement, params]) => + new Promise((resolve, reject) => { + transaction.executeSql( + statement, + params, + () => { + resolve(null); + }, + (_, error) => { + reject(error); + logger.warn(error); + return true; + } + ); + }) + ) + ); + if (deleteParameterizedStatements) { + await Promise.all( + [...deleteParameterizedStatements].map( + ([statement, params]) => + new Promise((resolve, reject) => + transaction.executeSql( + statement, + params, + () => { + resolve(null); + }, + (_, error) => { + reject(error); + logger.warn(error); + return true; + } + ) + ) + ) + ); + } + resolveTransaction(null); + } catch (error) { + rejectTransaction(error); + logger.warn(error); + } + }); + }); + } + + public selectAndDelete( + queryParameterizedStatement: ParameterizedStatement, + deleteParameterizedStatement: ParameterizedStatement + ): Promise { + const [queryStatement, queryParams] = queryParameterizedStatement; + const [deleteStatement, deleteParams] = deleteParameterizedStatement; + + return new Promise((resolveTransaction, rejectTransaction) => { + this.db.transaction(async transaction => { + try { + const result: T[] = await new Promise((resolve, reject) => { + transaction.executeSql( + queryStatement, + queryParams, + (_, result) => { + resolve(result.rows._array || []); + }, + (_, error) => { + reject(error); + logger.warn(error); + return true; + } + ); + }); + await new Promise((resolve, reject) => { + transaction.executeSql( + deleteStatement, + deleteParams, + () => { + resolve(null); + }, + (_, error) => { + reject(error); + logger.warn(error); + return true; + } + ); + }); + resolveTransaction(result); + } catch (error) { + rejectTransaction(error); + logger.warn(error); + } + }); + }); + } + + private executeStatements(statements: string[]): Promise { + return new Promise((resolveTransaction, rejectTransaction) => { + this.db.transaction(async transaction => { + try { + await Promise.all( + statements.map( + statement => + new Promise((resolve, reject) => { + transaction.executeSql( + statement, + [], + () => { + resolve(null); + }, + (_, error) => { + reject(error); + return true; + } + ); + }) + ) + ); + resolveTransaction(null); + } catch (error) { + rejectTransaction(error); + logger.warn(error); + } + }); + }); + } + + private async closeDB() { + if (this.db) { + logger.debug('Closing Database'); + // closing database is not supported by expo-sqlite. + // Workaround is to access the private db variable and call the close() method. + await (this.db as any)._db.close(); + logger.debug('Database closed'); + this.db = undefined; + } + } +} + +export default ExpoSQLiteDatabase; diff --git a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts index 6b7254acc50..61661be67ad 100644 --- a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts +++ b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts @@ -1,478 +1,8 @@ -import { ConsoleLogger as Logger } from '@aws-amplify/core'; +import { CommonSQLiteAdapter } from '../common/CommonSQLiteAdapter'; import SQLiteDatabase from './SQLiteDatabase'; -import { - generateSchemaStatements, - queryByIdStatement, - modelUpdateStatement, - modelInsertStatement, - queryAllStatement, - queryOneStatement, - deleteByIdStatement, - deleteByPredicateStatement, - ParameterizedStatement, -} from './SQLiteUtils'; -import { - StorageAdapter, - ModelInstanceCreator, - ModelPredicateCreator, - ModelSortPredicateCreator, - InternalSchema, - isPredicateObj, - ModelInstanceMetadata, - ModelPredicate, - NamespaceResolver, - OpType, - PaginationInput, - PersistentModel, - PersistentModelConstructor, - PredicateObject, - PredicatesGroup, - QueryOne, - utils, -} from '@aws-amplify/datastore'; +const SQLiteAdapter: CommonSQLiteAdapter = new CommonSQLiteAdapter( + new SQLiteDatabase() +); -const { traverseModel, validatePredicate, isModelConstructor } = utils; - -const logger = new Logger('DataStore'); -export class SQLiteAdapter implements StorageAdapter { - private schema: InternalSchema; - private namespaceResolver: NamespaceResolver; - private modelInstanceCreator: ModelInstanceCreator; - private getModelConstructorByModelName: ( - namsespaceName: string, - modelName: string - ) => PersistentModelConstructor; - private db: SQLiteDatabase; - private initPromise: Promise; - private resolve: (value?: any) => void; - private reject: (value?: any) => void; - - public async setUp( - theSchema: InternalSchema, - namespaceResolver: NamespaceResolver, - modelInstanceCreator: ModelInstanceCreator, - getModelConstructorByModelName: ( - namsespaceName: string, - modelName: string - ) => PersistentModelConstructor - ) { - if (!this.initPromise) { - this.initPromise = new Promise((res, rej) => { - this.resolve = res; - this.reject = rej; - }); - } else { - await this.initPromise; - return; - } - this.schema = theSchema; - this.namespaceResolver = namespaceResolver; - this.modelInstanceCreator = modelInstanceCreator; - this.getModelConstructorByModelName = getModelConstructorByModelName; - - try { - if (!this.db) { - this.db = new SQLiteDatabase(); - await this.db.init(); - - const statements = generateSchemaStatements(this.schema); - await this.db.createSchema(statements); - this.resolve(); - } - } catch (error) { - this.reject(error); - } - } - - async clear(): Promise { - await this.db.clear(); - - this.db = undefined; - this.initPromise = undefined; - } - - async save( - model: T, - condition?: ModelPredicate - ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { - const modelConstructor = Object.getPrototypeOf(model) - .constructor as PersistentModelConstructor; - const { name: tableName } = modelConstructor; - const connectedModels = traverseModel( - modelConstructor.name, - model, - this.schema.namespaces[this.namespaceResolver(modelConstructor)], - this.modelInstanceCreator, - this.getModelConstructorByModelName - ); - - const connectionStoreNames = Object.values(connectedModels).map( - ({ modelName, item, instance }) => { - return { modelName, item, instance }; - } - ); - - const [queryStatement, params] = queryByIdStatement(model.id, tableName); - - const fromDB = await this.db.get(queryStatement, params); - - if (condition && fromDB) { - const predicates = ModelPredicateCreator.getPredicates(condition); - const { predicates: predicateObjs, type } = predicates; - - const isValid = validatePredicate(fromDB, type, predicateObjs); - - if (!isValid) { - const msg = 'Conditional update failed'; - logger.error(msg, { model: fromDB, condition: predicateObjs }); - - throw new Error(msg); - } - } - - const result: [T, OpType.INSERT | OpType.UPDATE][] = []; - const saveStatements = new Set(); - - for await (const resItem of connectionStoreNames) { - const { modelName, item, instance } = resItem; - const { id } = item; - - const [queryStatement, params] = queryByIdStatement(id, modelName); - const fromDB = await this.db.get(queryStatement, params); - - const opType: OpType = - fromDB === undefined ? OpType.INSERT : OpType.UPDATE; - - const saveStatement = fromDB - ? modelUpdateStatement(instance, modelName) - : modelInsertStatement(instance, modelName); - - if (id === model.id || opType === OpType.INSERT) { - saveStatements.add(saveStatement); - result.push([instance, opType]); - } - } - - await this.db.batchSave(saveStatements); - return result; - } - - private async load( - namespaceName: string, - srcModelName: string, - records: T[] - ): Promise { - const namespace = this.schema.namespaces[namespaceName]; - const relations = namespace.relationships[srcModelName].relationTypes; - const connectionTableNames = relations.map(({ modelName }) => modelName); - - const modelConstructor = this.getModelConstructorByModelName( - namespaceName, - srcModelName - ); - - if (connectionTableNames.length === 0) { - return records.map(record => - this.modelInstanceCreator(modelConstructor, record) - ); - } - - for await (const relation of relations) { - const { - fieldName, - modelName: tableName, - targetName, - relationType, - } = relation; - - const modelConstructor = this.getModelConstructorByModelName( - namespaceName, - tableName - ); - - // TODO: use SQL JOIN instead - switch (relationType) { - case 'HAS_ONE': - for await (const recordItem of records) { - const getByfield = recordItem[targetName] ? targetName : fieldName; - if (!recordItem[getByfield]) break; - - const [queryStatement, params] = queryByIdStatement( - recordItem[getByfield], - tableName - ); - - const connectionRecord = await this.db.get(queryStatement, params); - - recordItem[fieldName] = - connectionRecord && - this.modelInstanceCreator(modelConstructor, connectionRecord); - } - - break; - case 'BELONGS_TO': - for await (const recordItem of records) { - if (recordItem[targetName]) { - const [queryStatement, params] = queryByIdStatement( - recordItem[targetName], - tableName - ); - const connectionRecord = await this.db.get( - queryStatement, - params - ); - - recordItem[fieldName] = - connectionRecord && - this.modelInstanceCreator(modelConstructor, connectionRecord); - delete recordItem[targetName]; - } - } - - break; - case 'HAS_MANY': - // TODO: Lazy loading - break; - default: - const _: never = relationType as never; - throw new Error(`invalid relation type ${relationType}`); - break; - } - } - - return records.map(record => - this.modelInstanceCreator(modelConstructor, record) - ); - } - - async query( - modelConstructor: PersistentModelConstructor, - predicate?: ModelPredicate, - pagination?: PaginationInput - ): Promise { - const { name: tableName } = modelConstructor; - const namespaceName = this.namespaceResolver(modelConstructor); - - const predicates = - predicate && ModelPredicateCreator.getPredicates(predicate); - const sortPredicates = - pagination && - pagination.sort && - ModelSortPredicateCreator.getPredicates(pagination.sort); - const limit = pagination && pagination.limit; - const page = limit && pagination.page; - - const queryById = predicates && this.idFromPredicate(predicates); - - const records: T[] = await (async () => { - if (queryById) { - const record = await this.getById(tableName, queryById); - return record ? [record] : []; - } - - const [queryStatement, params] = queryAllStatement( - tableName, - predicates, - sortPredicates, - limit, - page - ); - - return await this.db.getAll(queryStatement, params); - })(); - - return await this.load(namespaceName, modelConstructor.name, records); - } - - private async getById( - tableName: string, - id: string - ): Promise { - const [queryStatement, params] = queryByIdStatement(id, tableName); - const record = await this.db.get(queryStatement, params); - return record; - } - - private idFromPredicate( - predicates: PredicatesGroup - ) { - const { predicates: predicateObjs } = predicates; - const idPredicate = - predicateObjs.length === 1 && - (predicateObjs.find( - p => isPredicateObj(p) && p.field === 'id' && p.operator === 'eq' - ) as PredicateObject); - - return idPredicate && idPredicate.operand; - } - - async queryOne( - modelConstructor: PersistentModelConstructor, - firstOrLast: QueryOne = QueryOne.FIRST - ): Promise { - const { name: tableName } = modelConstructor; - const [queryStatement, params] = queryOneStatement(firstOrLast, tableName); - - const result = await this.db.get(queryStatement, params); - - const modelInstance = - result && this.modelInstanceCreator(modelConstructor, result); - - return modelInstance; - } - - // Currently does not cascade - // TODO: use FKs in relations and have `ON DELETE CASCADE` set - // For Has Many and Has One relations to have SQL handle cascades automatically - async delete( - modelOrModelConstructor: T | PersistentModelConstructor, - condition?: ModelPredicate - ): Promise<[T[], T[]]> { - if (isModelConstructor(modelOrModelConstructor)) { - const modelConstructor = modelOrModelConstructor; - const namespaceName = this.namespaceResolver(modelConstructor); - const { name: tableName } = modelConstructor; - - const predicates = - condition && ModelPredicateCreator.getPredicates(condition); - - const queryStatement = queryAllStatement(tableName, predicates); - const deleteStatement = deleteByPredicateStatement(tableName, predicates); - - const models = await this.db.selectAndDelete( - queryStatement, - deleteStatement - ); - - const modelInstances = await this.load( - namespaceName, - modelConstructor.name, - models - ); - - return [modelInstances, modelInstances]; - } else { - const model = modelOrModelConstructor as T; - const modelConstructor = Object.getPrototypeOf(model) - .constructor as PersistentModelConstructor; - const { name: tableName } = modelConstructor; - - if (condition) { - const [queryStatement, params] = queryByIdStatement( - model.id, - tableName - ); - - const fromDB = await this.db.get(queryStatement, params); - - if (fromDB === undefined) { - const msg = 'Model instance not found in storage'; - logger.warn(msg, { model }); - - return [[model], []]; - } - - const predicates = ModelPredicateCreator.getPredicates(condition); - const { predicates: predicateObjs, type } = predicates; - - const isValid = validatePredicate(fromDB, type, predicateObjs); - - if (!isValid) { - const msg = 'Conditional update failed'; - logger.error(msg, { model: fromDB, condition: predicateObjs }); - - throw new Error(msg); - } - - const [deleteStatement, deleteParams] = deleteByIdStatement( - model.id, - tableName - ); - await this.db.save(deleteStatement, deleteParams); - return [[model], [model]]; - } else { - const [deleteStatement, params] = deleteByIdStatement( - model.id, - tableName - ); - await this.db.save(deleteStatement, params); - return [[model], [model]]; - } - } - } - - async batchSave( - modelConstructor: PersistentModelConstructor, - items: ModelInstanceMetadata[] - ): Promise<[T, OpType][]> { - const { name: tableName } = modelConstructor; - - const result: [T, OpType][] = []; - - const itemsToSave: T[] = []; - // To determine whether an item should result in an insert or update operation - // We first need to query the local DB on the item id - const queryStatements = new Set(); - // Deletes don't need to be queried first, because if the item doesn't exist, - // the delete operation will be a no-op - const deleteStatements = new Set(); - const saveStatements = new Set(); - - for (const item of items) { - const connectedModels = traverseModel( - modelConstructor.name, - this.modelInstanceCreator(modelConstructor, item), - this.schema.namespaces[this.namespaceResolver(modelConstructor)], - this.modelInstanceCreator, - this.getModelConstructorByModelName - ); - - const { id, _deleted } = item; - - const { instance } = connectedModels.find( - ({ instance }) => instance.id === id - ); - - if (_deleted) { - // create the delete statements right away - const deleteStatement = deleteByIdStatement(instance.id, tableName); - deleteStatements.add(deleteStatement); - result.push([(item), OpType.DELETE]); - } else { - // query statements for the saves at first - const queryStatement = queryByIdStatement(id, tableName); - queryStatements.add(queryStatement); - // combination of insert and update items - itemsToSave.push(instance); - } - } - - // returns the query results for each of the save items - const queryResponses = await this.db.batchQuery(queryStatements); - - queryResponses.forEach((response, idx) => { - if (response === undefined) { - const insertStatement = modelInsertStatement( - itemsToSave[idx], - tableName - ); - saveStatements.add(insertStatement); - result.push([(itemsToSave[idx]), OpType.INSERT]); - } else { - const updateStatement = modelUpdateStatement( - itemsToSave[idx], - tableName - ); - saveStatements.add(updateStatement); - result.push([(itemsToSave[idx]), OpType.UPDATE]); - } - }); - - // perform all of the insert/update/delete operations in a single transaction - await this.db.batchSave(saveStatements, deleteStatements); - return result; - } -} - -export default new SQLiteAdapter(); +export default SQLiteAdapter; diff --git a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts index f8d6cdbf7f1..f5c5b08839a 100644 --- a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts +++ b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts @@ -1,7 +1,8 @@ import SQLite from 'react-native-sqlite-storage'; import { ConsoleLogger as Logger } from '@aws-amplify/core'; import { PersistentModel } from '@aws-amplify/datastore'; -import { ParameterizedStatement } from './SQLiteUtils'; +import { DB_NAME } from '../common/constants'; +import { CommonSQLiteDatabase, ParameterizedStatement } from '../common/types'; const logger = new Logger('SQLiteDatabase'); @@ -11,13 +12,6 @@ if (Logger.LOG_LEVEL === 'DEBUG') { SQLite.DEBUG(true); } -const DB_NAME = 'AmplifyDatastore'; -const DB_DISPLAYNAME = 'AWS Amplify DataStore SQLite Database'; - -// TODO: make these configurable -const DB_SIZE = 200000; -const DB_VERSION = '1.0'; - /* Note: @@ -31,47 +25,41 @@ get the result of an `executeSql` command inside of a transaction */ -class SQLiteDatabase { +class SQLiteDatabase implements CommonSQLiteDatabase { private db: SQLite.SQLiteDatabase; public async init(): Promise { - this.db = await SQLite.openDatabase( - DB_NAME, - DB_VERSION, - DB_DISPLAYNAME, - DB_SIZE - ); + // only open database once. + if (!this.db) { + this.db = await SQLite.openDatabase({ + name: DB_NAME, + location: 'default', + }); + } } - public async createSchema(statements: string[]) { + public async createSchema(statements: string[]): Promise { return await this.executeStatements(statements); } - public async clear() { + public async clear(): Promise { await this.closeDB(); logger.debug('Deleting database'); - await SQLite.deleteDatabase(DB_NAME); + await SQLite.deleteDatabase({ name: DB_NAME, location: 'default' }); logger.debug('Database deleted'); } public async get( statement: string, - params: any[] + params: (string | number)[] ): Promise { - const [resultSet] = await this.db.executeSql(statement, params); - const result = - resultSet && - resultSet.rows && - resultSet.rows.length && - resultSet.rows.raw && - resultSet.rows.raw(); - - return result?.[0] || undefined; + const results: T[] = await this.getAll(statement, params); + return results[0]; } public async getAll( statement: string, - params: any[] + params: (string | number)[] ): Promise { const [resultSet] = await this.db.executeSql(statement, params); const result = @@ -84,19 +72,24 @@ class SQLiteDatabase { return result || []; } - public async save(statement: string, params: any[]): Promise { + public async save( + statement: string, + params: (string | number)[] + ): Promise { await this.db.executeSql(statement, params); } - public async batchQuery(queryStatements: Set) { + public async batchQuery( + queryParameterizedStatements: Set + ): Promise { const results = []; - await this.db.readTransaction(function (tx) { - for (const [statement, params] of queryStatements) { + await this.db.readTransaction(tx => { + for (const [statement, params] of queryParameterizedStatements) { tx.executeSql( statement, params, - function (_tx, res) { + (_, res) => { results.push(res.rows.raw()[0]); }, logger.warn @@ -108,35 +101,37 @@ class SQLiteDatabase { } public async batchSave( - saveStatements: Set, - deleteStatements?: Set - ) { - await this.db.transaction(function (tx) { - for (const [statement, params] of saveStatements) { + saveParameterizedStatements: Set, + deleteParameterizedStatements?: Set + ): Promise { + await this.db.transaction(tx => { + for (const [statement, params] of saveParameterizedStatements) { tx.executeSql(statement, params); } - if (deleteStatements) { - for (const [statement, params] of deleteStatements) { + }); + if (deleteParameterizedStatements) { + await this.db.transaction(tx => { + for (const [statement, params] of deleteParameterizedStatements) { tx.executeSql(statement, params); } - } - }); + }); + } } - public async selectAndDelete( - query: ParameterizedStatement, - _delete: ParameterizedStatement - ) { - let results = []; + public async selectAndDelete( + queryParameterizedStatement: ParameterizedStatement, + deleteParameterizedStatement: ParameterizedStatement + ): Promise { + let results: T[] = []; - const [queryStatement, queryParams] = query; - const [deleteStatement, deleteParams] = _delete; + const [queryStatement, queryParams] = queryParameterizedStatement; + const [deleteStatement, deleteParams] = deleteParameterizedStatement; - await this.db.transaction(function (tx) { + await this.db.transaction(tx => { tx.executeSql( queryStatement, queryParams, - function (_tx, res) { + (_, res) => { results = res.rows.raw(); }, logger.warn @@ -148,7 +143,7 @@ class SQLiteDatabase { } private async executeStatements(statements: string[]): Promise { - return await this.db.transaction(function (tx) { + await this.db.transaction(tx => { for (const statement of statements) { tx.executeSql(statement); } @@ -160,6 +155,7 @@ class SQLiteDatabase { logger.debug('Closing Database'); await this.db.close(); logger.debug('Database closed'); + this.db = undefined; } } } diff --git a/packages/datastore-storage-adapter/src/SQLiteAdapter/types.ts b/packages/datastore-storage-adapter/src/SQLiteAdapter/types.ts deleted file mode 100644 index 38e6cc96365..00000000000 --- a/packages/datastore-storage-adapter/src/SQLiteAdapter/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GraphQLScalarType } from '@aws-amplify/datastore'; - -export function getSQLiteType( - scalar: keyof Omit< - typeof GraphQLScalarType, - 'getJSType' | 'getValidationFunction' | 'getSQLiteType' - > -): 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' { - switch (scalar) { - case 'Boolean': - case 'Int': - case 'AWSTimestamp': - return 'INTEGER'; - case 'ID': - case 'String': - case 'AWSDate': - case 'AWSTime': - case 'AWSDateTime': - case 'AWSEmail': - case 'AWSJSON': - case 'AWSURL': - case 'AWSPhone': - case 'AWSIPAddress': - return 'TEXT'; - case 'Float': - return 'REAL'; - default: - const _: never = scalar as never; - throw new Error(`unknown type ${scalar as string}`); - } -} diff --git a/packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts b/packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts new file mode 100644 index 00000000000..d2d2a83d8cc --- /dev/null +++ b/packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts @@ -0,0 +1,479 @@ +import { ConsoleLogger as Logger } from '@aws-amplify/core'; +import { + generateSchemaStatements, + queryByIdStatement, + modelUpdateStatement, + modelInsertStatement, + queryAllStatement, + queryOneStatement, + deleteByIdStatement, + deleteByPredicateStatement, +} from '../common/SQLiteUtils'; + +import { + StorageAdapter, + ModelInstanceCreator, + ModelPredicateCreator, + ModelSortPredicateCreator, + InternalSchema, + isPredicateObj, + ModelInstanceMetadata, + ModelPredicate, + NamespaceResolver, + OpType, + PaginationInput, + PersistentModel, + PersistentModelConstructor, + PredicateObject, + PredicatesGroup, + QueryOne, + utils, +} from '@aws-amplify/datastore'; +import { CommonSQLiteDatabase, ParameterizedStatement } from './types'; + +const { traverseModel, validatePredicate, isModelConstructor } = utils; + +const logger = new Logger('DataStore'); + +export class CommonSQLiteAdapter implements StorageAdapter { + private schema: InternalSchema; + private namespaceResolver: NamespaceResolver; + private modelInstanceCreator: ModelInstanceCreator; + private getModelConstructorByModelName: ( + namsespaceName: string, + modelName: string + ) => PersistentModelConstructor; + private db: CommonSQLiteDatabase; + private initPromise: Promise; + private resolve: (value?: any) => void; + private reject: (value?: any) => void; + + constructor(db: CommonSQLiteDatabase) { + this.db = db; + } + + public async setUp( + theSchema: InternalSchema, + namespaceResolver: NamespaceResolver, + modelInstanceCreator: ModelInstanceCreator, + getModelConstructorByModelName: ( + namsespaceName: string, + modelName: string + ) => PersistentModelConstructor + ) { + if (!this.initPromise) { + this.initPromise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } else { + await this.initPromise; + return; + } + this.schema = theSchema; + this.namespaceResolver = namespaceResolver; + this.modelInstanceCreator = modelInstanceCreator; + this.getModelConstructorByModelName = getModelConstructorByModelName; + + try { + await this.db.init(); + const statements = generateSchemaStatements(this.schema); + await this.db.createSchema(statements); + + this.resolve(); + } catch (error) { + this.reject(error); + } + } + + async clear(): Promise { + await this.db.clear(); + + this.initPromise = undefined; + } + + async save( + model: T, + condition?: ModelPredicate + ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { + const modelConstructor = Object.getPrototypeOf(model) + .constructor as PersistentModelConstructor; + const { name: tableName } = modelConstructor; + const connectedModels = traverseModel( + modelConstructor.name, + model, + this.schema.namespaces[this.namespaceResolver(modelConstructor)], + this.modelInstanceCreator, + this.getModelConstructorByModelName + ); + const connectionStoreNames = Object.values(connectedModels).map( + ({ modelName, item, instance }) => { + return { modelName, item, instance }; + } + ); + + const [queryStatement, params] = queryByIdStatement(model.id, tableName); + + const fromDB = await this.db.get(queryStatement, params); + + if (condition && fromDB) { + const predicates = ModelPredicateCreator.getPredicates(condition); + const { predicates: predicateObjs, type } = predicates; + + const isValid = validatePredicate(fromDB, type, predicateObjs); + + if (!isValid) { + const msg = 'Conditional update failed'; + logger.error(msg, { model: fromDB, condition: predicateObjs }); + + throw new Error(msg); + } + } + + const result: [T, OpType.INSERT | OpType.UPDATE][] = []; + const saveStatements = new Set(); + + for await (const resItem of connectionStoreNames) { + const { modelName, item, instance } = resItem; + const { id } = item; + + const [queryStatement, params] = queryByIdStatement(id, modelName); + const fromDB = await this.db.get(queryStatement, params); + + const opType: OpType = + fromDB === undefined ? OpType.INSERT : OpType.UPDATE; + + const saveStatement = fromDB + ? modelUpdateStatement(instance, modelName) + : modelInsertStatement(instance, modelName); + + if (id === model.id || opType === OpType.INSERT) { + saveStatements.add(saveStatement); + result.push([instance, opType]); + } + } + + await this.db.batchSave(saveStatements); + + return result; + } + + private async load( + namespaceName: string, + srcModelName: string, + records: T[] + ): Promise { + const namespace = this.schema.namespaces[namespaceName]; + const relations = namespace.relationships[srcModelName].relationTypes; + const connectionTableNames = relations.map(({ modelName }) => modelName); + + const modelConstructor = this.getModelConstructorByModelName( + namespaceName, + srcModelName + ); + + if (connectionTableNames.length === 0) { + return records.map(record => + this.modelInstanceCreator(modelConstructor, record) + ); + } + + for await (const relation of relations) { + const { + fieldName, + modelName: tableName, + targetName, + relationType, + } = relation; + + const modelConstructor = this.getModelConstructorByModelName( + namespaceName, + tableName + ); + + // TODO: use SQL JOIN instead + switch (relationType) { + case 'HAS_ONE': + for await (const recordItem of records) { + const getByfield = recordItem[targetName] ? targetName : fieldName; + if (!recordItem[getByfield]) break; + + const [queryStatement, params] = queryByIdStatement( + recordItem[getByfield], + tableName + ); + + const connectionRecord = await this.db.get(queryStatement, params); + + recordItem[fieldName] = + connectionRecord && + this.modelInstanceCreator(modelConstructor, connectionRecord); + } + + break; + case 'BELONGS_TO': + for await (const recordItem of records) { + if (recordItem[targetName]) { + const [queryStatement, params] = queryByIdStatement( + recordItem[targetName], + tableName + ); + const connectionRecord = await this.db.get( + queryStatement, + params + ); + + recordItem[fieldName] = + connectionRecord && + this.modelInstanceCreator(modelConstructor, connectionRecord); + delete recordItem[targetName]; + } + } + + break; + case 'HAS_MANY': + // TODO: Lazy loading + break; + default: + const _: never = relationType as never; + throw new Error(`invalid relation type ${relationType}`); + break; + } + } + + return records.map(record => + this.modelInstanceCreator(modelConstructor, record) + ); + } + + async query( + modelConstructor: PersistentModelConstructor, + predicate?: ModelPredicate, + pagination?: PaginationInput + ): Promise { + const { name: tableName } = modelConstructor; + const namespaceName = this.namespaceResolver(modelConstructor); + + const predicates = + predicate && ModelPredicateCreator.getPredicates(predicate); + const sortPredicates = + pagination && + pagination.sort && + ModelSortPredicateCreator.getPredicates(pagination.sort); + const limit = pagination && pagination.limit; + const page = limit && pagination.page; + + const queryById = predicates && this.idFromPredicate(predicates); + + const records: T[] = await (async () => { + if (queryById) { + const record = await this.getById(tableName, queryById); + return record ? [record] : []; + } + + const [queryStatement, params] = queryAllStatement( + tableName, + predicates, + sortPredicates, + limit, + page + ); + + return await this.db.getAll(queryStatement, params); + })(); + + return await this.load(namespaceName, modelConstructor.name, records); + } + + private async getById( + tableName: string, + id: string + ): Promise { + const [queryStatement, params] = queryByIdStatement(id, tableName); + const record = await this.db.get(queryStatement, params); + + return record; + } + + private idFromPredicate( + predicates: PredicatesGroup + ) { + const { predicates: predicateObjs } = predicates; + const idPredicate = + predicateObjs.length === 1 && + (predicateObjs.find( + p => isPredicateObj(p) && p.field === 'id' && p.operator === 'eq' + ) as PredicateObject); + + return idPredicate && idPredicate.operand; + } + + async queryOne( + modelConstructor: PersistentModelConstructor, + firstOrLast: QueryOne = QueryOne.FIRST + ): Promise { + const { name: tableName } = modelConstructor; + const [queryStatement, params] = queryOneStatement(firstOrLast, tableName); + + const result = await this.db.get(queryStatement, params); + + const modelInstance = + result && this.modelInstanceCreator(modelConstructor, result); + + return modelInstance; + } + + // Currently does not cascade + // TODO: use FKs in relations and have `ON DELETE CASCADE` set + // For Has Many and Has One relations to have SQL handle cascades automatically + async delete( + modelOrModelConstructor: T | PersistentModelConstructor, + condition?: ModelPredicate + ): Promise<[T[], T[]]> { + if (isModelConstructor(modelOrModelConstructor)) { + const modelConstructor = modelOrModelConstructor; + const namespaceName = this.namespaceResolver(modelConstructor); + const { name: tableName } = modelConstructor; + + const predicates = + condition && ModelPredicateCreator.getPredicates(condition); + + const queryStatement = queryAllStatement(tableName, predicates); + const deleteStatement = deleteByPredicateStatement(tableName, predicates); + + const models = await this.db.selectAndDelete( + queryStatement, + deleteStatement + ); + + const modelInstances = await this.load( + namespaceName, + modelConstructor.name, + models + ); + + return [modelInstances, modelInstances]; + } else { + const model = modelOrModelConstructor as T; + const modelConstructor = Object.getPrototypeOf(model) + .constructor as PersistentModelConstructor; + const { name: tableName } = modelConstructor; + + if (condition) { + const [queryStatement, params] = queryByIdStatement( + model.id, + tableName + ); + + const fromDB = await this.db.get(queryStatement, params); + + if (fromDB === undefined) { + const msg = 'Model instance not found in storage'; + logger.warn(msg, { model }); + + return [[model], []]; + } + + const predicates = ModelPredicateCreator.getPredicates(condition); + const { predicates: predicateObjs, type } = predicates; + + const isValid = validatePredicate(fromDB, type, predicateObjs); + + if (!isValid) { + const msg = 'Conditional update failed'; + logger.error(msg, { model: fromDB, condition: predicateObjs }); + + throw new Error(msg); + } + + const [deleteStatement, deleteParams] = deleteByIdStatement( + model.id, + tableName + ); + await this.db.save(deleteStatement, deleteParams); + + return [[model], [model]]; + } else { + const [deleteStatement, params] = deleteByIdStatement( + model.id, + tableName + ); + await this.db.save(deleteStatement, params); + + return [[model], [model]]; + } + } + } + + async batchSave( + modelConstructor: PersistentModelConstructor, + items: ModelInstanceMetadata[] + ): Promise<[T, OpType][]> { + const { name: tableName } = modelConstructor; + const result: [T, OpType][] = []; + + const itemsToSave: T[] = []; + // To determine whether an item should result in an insert or update operation + // We first need to query the local DB on the item id + const queryStatements = new Set(); + // Deletes don't need to be queried first, because if the item doesn't exist, + // the delete operation will be a no-op + const deleteStatements = new Set(); + const saveStatements = new Set(); + + for (const item of items) { + const connectedModels = traverseModel( + modelConstructor.name, + this.modelInstanceCreator(modelConstructor, item), + this.schema.namespaces[this.namespaceResolver(modelConstructor)], + this.modelInstanceCreator, + this.getModelConstructorByModelName + ); + + const { id, _deleted } = item; + + const { instance } = connectedModels.find( + ({ instance }) => instance.id === id + ); + + if (_deleted) { + // create the delete statements right away + const deleteStatement = deleteByIdStatement(instance.id, tableName); + deleteStatements.add(deleteStatement); + result.push([(item), OpType.DELETE]); + } else { + // query statements for the saves at first + const queryStatement = queryByIdStatement(id, tableName); + queryStatements.add(queryStatement); + // combination of insert and update items + itemsToSave.push(instance); + } + } + + // returns the query results for each of the save items + const queryResponses = await this.db.batchQuery(queryStatements); + + queryResponses.forEach((response, idx) => { + if (response === undefined) { + const insertStatement = modelInsertStatement( + itemsToSave[idx], + tableName + ); + saveStatements.add(insertStatement); + result.push([(itemsToSave[idx]), OpType.INSERT]); + } else { + const updateStatement = modelUpdateStatement( + itemsToSave[idx], + tableName + ); + saveStatements.add(updateStatement); + result.push([(itemsToSave[idx]), OpType.UPDATE]); + } + }); + + // perform all of the insert/update/delete operations in a single transaction + await this.db.batchSave(saveStatements, deleteStatements); + + return result; + } +} diff --git a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteUtils.ts b/packages/datastore-storage-adapter/src/common/SQLiteUtils.ts similarity index 94% rename from packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteUtils.ts rename to packages/datastore-storage-adapter/src/common/SQLiteUtils.ts index 3aa1ecfb1c7..ecc29d79222 100644 --- a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteUtils.ts +++ b/packages/datastore-storage-adapter/src/common/SQLiteUtils.ts @@ -16,14 +16,13 @@ import { ModelAttributeAuth, ModelAuthRule, utils, + GraphQLScalarType, } from '@aws-amplify/datastore'; -import { getSQLiteType } from './types'; +import { ParameterizedStatement } from './types'; const { USER, isNonModelConstructor, isModelConstructor } = utils; -export type ParameterizedStatement = [string, any[]]; - const keysFromModel = model => Object.keys(model) .map(k => `"${k}"`) @@ -72,6 +71,36 @@ function prepareValueForDML(value: unknown): any { return `${value}`; } +export function getSQLiteType( + scalar: keyof Omit< + typeof GraphQLScalarType, + 'getJSType' | 'getValidationFunction' | 'getSQLiteType' + > +): 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' { + switch (scalar) { + case 'Boolean': + case 'Int': + case 'AWSTimestamp': + return 'INTEGER'; + case 'ID': + case 'String': + case 'AWSDate': + case 'AWSTime': + case 'AWSDateTime': + case 'AWSEmail': + case 'AWSJSON': + case 'AWSURL': + case 'AWSPhone': + case 'AWSIPAddress': + return 'TEXT'; + case 'Float': + return 'REAL'; + default: + const _: never = scalar as never; + throw new Error(`unknown type ${scalar as string}`); + } +} + export function generateSchemaStatements(schema: InternalSchema): string[] { return Object.keys(schema.namespaces).flatMap(namespaceName => { const namespace = schema.namespaces[namespaceName]; diff --git a/packages/datastore-storage-adapter/src/common/constants.ts b/packages/datastore-storage-adapter/src/common/constants.ts new file mode 100644 index 00000000000..1890d7f9d68 --- /dev/null +++ b/packages/datastore-storage-adapter/src/common/constants.ts @@ -0,0 +1 @@ +export const DB_NAME = 'AmplifyDatastore'; diff --git a/packages/datastore-storage-adapter/src/common/types.ts b/packages/datastore-storage-adapter/src/common/types.ts new file mode 100644 index 00000000000..84c63014371 --- /dev/null +++ b/packages/datastore-storage-adapter/src/common/types.ts @@ -0,0 +1,29 @@ +import { PersistentModel } from '@aws-amplify/datastore'; + +export interface CommonSQLiteDatabase { + init(): Promise; + createSchema(statements: string[]): Promise; + clear(): Promise; + get( + statement: string, + params: (string | number)[] + ): Promise; + getAll( + statement: string, + params: (string | number)[] + ): Promise; + save(statement: string, params: (string | number)[]): Promise; + batchQuery( + queryParameterizedStatement: Set + ): Promise; + batchSave( + saveParameterizedStatements: Set, + deleteParameterizedStatements?: Set + ): Promise; + selectAndDelete( + queryParameterizedStatement: ParameterizedStatement, + deleteParameterizedStatement: ParameterizedStatement + ): Promise; +} + +export type ParameterizedStatement = [string, any[]]; diff --git a/packages/datastore-storage-adapter/tslint.json b/packages/datastore-storage-adapter/tslint.json index 4b5dba47007..8eafab1d2b4 100644 --- a/packages/datastore-storage-adapter/tslint.json +++ b/packages/datastore-storage-adapter/tslint.json @@ -13,7 +13,7 @@ "space-before-function-paren": [ true, { - "anonymous": "always", + "anonymous": "never", "named": "never" } ], diff --git a/packages/datastore-storage-adapter/webpack.config.dev.js b/packages/datastore-storage-adapter/webpack.config.dev.js index 31512a1245b..768369c8530 100644 --- a/packages/datastore-storage-adapter/webpack.config.dev.js +++ b/packages/datastore-storage-adapter/webpack.config.dev.js @@ -2,5 +2,7 @@ var config = require('./webpack.config.js'); var entry = { 'aws-amplify-datastore-storage-adapter': './lib-esm/index.js', + 'aws-amplify-datastore-sqlite-adapter-expo': + './lib-esm/ExpoSQLiteAdapter/ExpoSQLiteAdapter.js', }; module.exports = Object.assign(config, { entry, mode: 'development' }); diff --git a/packages/datastore-storage-adapter/webpack.config.js b/packages/datastore-storage-adapter/webpack.config.js index ecc1d2fcb18..c88cbc5d740 100644 --- a/packages/datastore-storage-adapter/webpack.config.js +++ b/packages/datastore-storage-adapter/webpack.config.js @@ -1,10 +1,14 @@ module.exports = { entry: { 'aws-amplify-datastore-storage-adapter.min': './lib-esm/index.js', + 'aws-amplify-datastore-sqlite-adapter-expo.min': + './lib-esm/ExpoSQLiteAdapter/ExpoSQLiteAdapter.js', }, externals: [ '@aws-amplify/datastore', '@aws-amplify/core', + 'expo-file-system', + 'expo-sqlite', 'react-native-sqlite-storage', ], output: { diff --git a/packages/datastore/CHANGELOG.md b/packages/datastore/CHANGELOG.md index 1029fb4ce4a..c72b963ea48 100644 --- a/packages/datastore/CHANGELOG.md +++ b/packages/datastore/CHANGELOG.md @@ -3,6 +3,153 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.12.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.7...@aws-amplify/datastore@3.12.8) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/datastore + + + + + +## [3.12.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.6...@aws-amplify/datastore@3.12.7) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/datastore + + + + + +## [3.12.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.5...@aws-amplify/datastore@3.12.6) (2022-08-16) + + +### Bug Fixes + +* **datastore:** make di context fields private ([#10162](https://github.com/aws-amplify/amplify-js/issues/10162)) ([88a9ec9](https://github.com/aws-amplify/amplify-js/commit/88a9ec97fca2eb19c9cc9496b8b7d25b75f02073)) + + + + + +## [3.12.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.4...@aws-amplify/datastore@3.12.5) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/datastore + + + + + +## [3.12.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.3...@aws-amplify/datastore@3.12.4) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/datastore + + + + + +## [3.12.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.2...@aws-amplify/datastore@3.12.3) (2022-07-21) + + +### Bug Fixes + +* preserve ssr context when using DataStore ([#10088](https://github.com/aws-amplify/amplify-js/issues/10088)) ([a10d920](https://github.com/aws-amplify/amplify-js/commit/a10d920f7fb6199539fb8d9cec2cb4426dbfd47b)) + + + + + +## [3.12.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.1...@aws-amplify/datastore@3.12.2) (2022-07-07) + + +### Bug Fixes + +* decrease error handler verbosity on self recovering errors ([#10030](https://github.com/aws-amplify/amplify-js/issues/10030)) ([fb1f02c](https://github.com/aws-amplify/amplify-js/commit/fb1f02cfa914b81fe0411e8f4d654c69aed22385)) + + + + + +## [3.12.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.12.0...@aws-amplify/datastore@3.12.1) (2022-06-18) + + +### Bug Fixes + +* decrease error handler verbosity on self recovering errors ([#9987](https://github.com/aws-amplify/amplify-js/issues/9987)) ([67ccf09](https://github.com/aws-amplify/amplify-js/commit/67ccf09a93221a06d4560300cfd67fdd9efeda71)) + + +### Reverts + +* Revert "fix: decrease error handler verbosity on self recovering errors (#9987)" (#10004) ([eb73ad7](https://github.com/aws-amplify/amplify-js/commit/eb73ad70b3eee0632eaed4bae00f1d2179ae45b5)), closes [#9987](https://github.com/aws-amplify/amplify-js/issues/9987) [#10004](https://github.com/aws-amplify/amplify-js/issues/10004) + + + + + +# [3.12.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.11.3...@aws-amplify/datastore@3.12.0) (2022-06-15) + + +### Bug Fixes + +* **@aws-amplify/datastore:** adds missing fields to items sent through observe/observeQuery ([#9973](https://github.com/aws-amplify/amplify-js/issues/9973)) ([ca2a11b](https://github.com/aws-amplify/amplify-js/commit/ca2a11b5bc987e71ce3344058a4886bf067cb17b)) +* merge patches for consecutive copyOf ([#9936](https://github.com/aws-amplify/amplify-js/issues/9936)) ([d5dd9cb](https://github.com/aws-amplify/amplify-js/commit/d5dd9cb5bf46131fb046cfe55e4899444f9d789e)) +* **@aws-amplify/datastore:** fixes observeQuery not removing newly-filtered items from snapshot ([#9879](https://github.com/aws-amplify/amplify-js/issues/9879)) ([d1356b1](https://github.com/aws-amplify/amplify-js/commit/d1356b1e498eb71a4902892afbb41f6ff88abb6f)) + + +### Features + +* **datastore:** add error maps for error handler ([#9918](https://github.com/aws-amplify/amplify-js/issues/9918)) ([3a27096](https://github.com/aws-amplify/amplify-js/commit/3a270969b6e097eeed73368091ace191cbc05511)) + + + + + +## [3.11.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.11.2...@aws-amplify/datastore@3.11.3) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/datastore + + + + + +## [3.11.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.11.1...@aws-amplify/datastore@3.11.2) (2022-05-23) + + +### Bug Fixes + +* **@aws-amplify/datastore-storage-adapter:** remove extra, invalid sqlite mutations again ([#9921](https://github.com/aws-amplify/amplify-js/issues/9921)) ([00923cf](https://github.com/aws-amplify/amplify-js/commit/00923cfaeafcee97a0f54cc6aa04724f7155e75d)) + + + + + +## [3.11.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.11.0...@aws-amplify/datastore@3.11.1) (2022-05-12) + + +### Bug Fixes + +* add error for when schema is not initialized ([#9874](https://github.com/aws-amplify/amplify-js/issues/9874)) ([a63f0ee](https://github.com/aws-amplify/amplify-js/commit/a63f0eec70b96dba2d220f3eeb0c799af8622b5c)) + + + + + +# [3.11.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.10.0...@aws-amplify/datastore@3.11.0) (2022-05-03) + + +### Bug Fixes + +* add newly created models to IDB during migration ([#9754](https://github.com/aws-amplify/amplify-js/issues/9754)) ([58d7e00](https://github.com/aws-amplify/amplify-js/commit/58d7e003463e1cabe3a4bb5601a2cdf11736150d)) +* **@aws-amplify/datastore-storage-adapter:** SQLite adapter NULL handling and mutation queue management bugs ([#9813](https://github.com/aws-amplify/amplify-js/issues/9813)) ([fe691fd](https://github.com/aws-amplify/amplify-js/commit/fe691fd4f67adc6ac973dd12ca056563d0720d69)) + + +### Features + +* clear DataStore without first starting ([#9768](https://github.com/aws-amplify/amplify-js/issues/9768)) ([38bdabd](https://github.com/aws-amplify/amplify-js/commit/38bdabd5408e03595a90d673bbffd963cf432daa)) +* rework error handler ([#9861](https://github.com/aws-amplify/amplify-js/issues/9861)) ([6ae8d10](https://github.com/aws-amplify/amplify-js/commit/6ae8d10569abf24559436a46e1723825e6472489)) + + + + + # [3.10.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/datastore@3.9.0...@aws-amplify/datastore@3.10.0) (2022-04-14) diff --git a/packages/datastore/__tests__/AsyncStorageAdapter.test.ts b/packages/datastore/__tests__/AsyncStorageAdapter.test.ts index 49e4011a47c..f93961ba742 100644 --- a/packages/datastore/__tests__/AsyncStorageAdapter.test.ts +++ b/packages/datastore/__tests__/AsyncStorageAdapter.test.ts @@ -2,10 +2,12 @@ import AsyncStorageAdapter from '../src/storage/adapter/AsyncStorageAdapter'; import { DataStore as DataStoreType, initSchema as initSchemaType, + syncClasses, } from '../src/datastore/datastore'; import { PersistentModelConstructor, SortDirection } from '../src/types'; -import { Model, User, Profile, testSchema } from './helpers'; +import { pause, Model, User, Profile, testSchema } from './helpers'; import { Predicates } from '../src/predicates'; +import { addCommonQueryTests } from './commonAdapterTests'; let initSchema: typeof initSchemaType; let DataStore: typeof DataStoreType; @@ -17,6 +19,25 @@ describe('AsyncStorageAdapter tests', () => { jest.clearAllMocks(); }); + async function getMutations(adapter) { + await pause(250); + return await adapter.getAll('sync_MutationEvent'); + } + + async function clearOutbox(adapter) { + await pause(250); + return await adapter.delete(syncClasses['MutationEvent']); + } + + ({ initSchema, DataStore } = require('../src/datastore/datastore')); + addCommonQueryTests({ + initSchema, + DataStore, + storageAdapter: AsyncStorageAdapter, + getMutations, + clearOutbox, + }); + describe('Query', () => { let Model: PersistentModelConstructor; let model1Id: string; @@ -67,6 +88,10 @@ describe('AsyncStorageAdapter tests', () => { ); }); + afterAll(async () => { + await DataStore.clear(); + }); + it('Should call getById for query by id', async () => { const result = await DataStore.query(Model, model1Id); diff --git a/packages/datastore/__tests__/DataStore.ts b/packages/datastore/__tests__/DataStore.ts index c61f652c215..42795ea1fa2 100644 --- a/packages/datastore/__tests__/DataStore.ts +++ b/packages/datastore/__tests__/DataStore.ts @@ -13,7 +13,16 @@ import { PersistentModel, PersistentModelConstructor, } from '../src/types'; -import { Comment, Model, Post, Metadata, testSchema } from './helpers'; +import { + Comment, + Model, + Post, + Profile, + Metadata, + User, + testSchema, + pause, +} from './helpers'; let initSchema: typeof initSchemaType; let DataStore: typeof DataStoreType; @@ -265,14 +274,24 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => { let Comment: PersistentModelConstructor; let Post: PersistentModelConstructor; + let User: PersistentModelConstructor; + let Profile: PersistentModelConstructor; beforeEach(async () => { ({ initSchema, DataStore } = require('../src/datastore/datastore')); const classes = initSchema(testSchema()); - ({ Comment, Post } = classes as { + ({ Comment, Post, User, Profile } = classes as { Comment: PersistentModelConstructor; Post: PersistentModelConstructor; + User: PersistentModelConstructor; + Profile: PersistentModelConstructor; }); + + // This prevents pollution between tests. DataStore may have processes in + // flight that need to settle. If we stampede ahead before we do this, + // we can end up in very goofy states when we try to re-init the schema. + await DataStore.stop(); + await DataStore.start(); await DataStore.clear(); // Fully faking or mocking the sync engine would be pretty significant. @@ -349,25 +368,275 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => { } }); - test('publishes preexisting local data AND follows up with subsequent saves', async done => { + test('can filter items', async done => { + try { + const expecteds = [0, 5]; + + const sub = DataStore.observeQuery(Post, p => + p.title('contains', 'include') + ).subscribe(({ items }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (const item of items) { + expect(item.title).toMatch('include'); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + }); + + setTimeout(async () => { + for (let i = 0; i < 10; i++) { + await DataStore.save( + new Post({ + title: `the post ${i} - ${Boolean(i % 2) ? 'include' : 'omit'}`, + }) + ); + } + }, 100); + } catch (error) { + done(error); + } + }); + + // Fix for: https://github.com/aws-amplify/amplify-js/issues/9325 + test('can remove newly-unmatched items out of the snapshot on subsequent saves', async done => { + try { + // watch for post snapshots. + // the first "real" snapshot should include all five posts with "include" + // in the title. after the update to change ONE of those posts to "omit" instead, + // we should see a snapshot of 4 posts with the updated post removed. + const expecteds = [0, 4, 3]; + const sub = DataStore.observeQuery(Post, p => + p.title('contains', 'include') + ).subscribe(async ({ items }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (const item of items) { + expect(item.title).toMatch('include'); + } + + if (expecteds.length === 1) { + // After the second snapshot arrives, changes a single post from + // "the post # - include" + // to + // "edited post - omit" + + // This is intended to trigger a new, after-sync'd snapshot. + // This sanity-checks helps confirms we're testing what we think + // we're testing: + expect( + ((DataStore as any).sync as any).getModelSyncedStatus({}) + ).toBe(true); + + await pause(100); + const itemToEdit = ( + await DataStore.query(Post, p => p.title('contains', 'include')) + ).pop(); + await DataStore.save( + Post.copyOf(itemToEdit, draft => { + draft.title = 'second edited post - omit'; + }) + ); + } else if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + }); + + setTimeout(async () => { + // Creates posts like: + // + // "the post 0 - include" + // "the post 1 - omit" + // "the post 2 - include" + // "the post 3 - omit" + // + // etc. + // + for (let i = 0; i < 10; i++) { + await DataStore.save( + new Post({ + title: `the post ${i} - ${Boolean(i % 2) ? 'include' : 'omit'}`, + }) + ); + } + + // Changes a single post from + // "the post # - include" + // to + // "edited post - omit" + await pause(100); + ((DataStore as any).sync as any).getModelSyncedStatus = (model: any) => + true; + + // the first edit simulates a quick-turnaround update that gets + // applied while the first snapshot is still being generated + const itemToEdit = ( + await DataStore.query(Post, p => p.title('contains', 'include')) + ).pop(); + await DataStore.save( + Post.copyOf(itemToEdit, draft => { + draft.title = 'first edited post - omit'; + }) + ); + }, 100); + } catch (error) { + done(error); + } + }); + + test('publishes preexisting local data AND follows up with subsequent saves', done => { + (async () => { + try { + const expecteds = [5, 15]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toEqual(`the post ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + for (let i = 5; i < 15; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + }, 100); + } catch (error) { + done(error); + } + })(); + }); + + test('removes deleted items from the snapshot', done => { + (async () => { + try { + const expecteds = [5, 4]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toContain(`the post`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + const itemToDelete = (await DataStore.query(Post)).pop(); + await DataStore.delete(itemToDelete); + }, 100); + } catch (error) { + done(error); + } + })(); + }); + + test('removes deleted items from the snapshot with a predicate', done => { + (async () => { + try { + const expecteds = [5, 4]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post, p => + p.title('beginsWith', 'the post') + ).subscribe(({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toContain(`the post`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + }); + + setTimeout(async () => { + const itemToDelete = (await DataStore.query(Post)).pop(); + await DataStore.delete(itemToDelete); + }, 100); + } catch (error) { + done(error); + } + })(); + }); + + test('attaches related belongsTo properties consistently with query() on INSERT', async done => { try { const expecteds = [5, 15]; for (let i = 0; i < 5; i++) { await DataStore.save( - new Post({ - title: `the post ${i}`, + new Comment({ + content: `comment content ${i}`, + post: await DataStore.save( + new Post({ + title: `new post ${i}`, + }) + ), }) ); } - const sub = DataStore.observeQuery(Post).subscribe( + const sub = DataStore.observeQuery(Comment).subscribe( ({ items, isSynced }) => { const expected = expecteds.shift() || 0; expect(items.length).toBe(expected); for (let i = 0; i < expected; i++) { - expect(items[i].title).toEqual(`the post ${i}`); + expect(items[i].content).toEqual(`comment content ${i}`); + expect(items[i].post.title).toEqual(`new post ${i}`); } if (expecteds.length === 0) { @@ -380,8 +649,203 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => { setTimeout(async () => { for (let i = 5; i < 15; i++) { await DataStore.save( + new Comment({ + content: `comment content ${i}`, + post: await DataStore.save( + new Post({ + title: `new post ${i}`, + }) + ), + }) + ); + } + }, 100); + } catch (error) { + done(error); + } + }); + + test('attaches related hasOne properties consistently with query() on INSERT', async done => { + try { + const expecteds = [5, 15]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new User({ + name: `user ${i}`, + profile: await DataStore.save( + new Profile({ + firstName: `firstName ${i}`, + lastName: `lastName ${i}`, + }) + ), + }) + ); + } + + const sub = DataStore.observeQuery(User).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].name).toEqual(`user ${i}`); + expect(items[i].profile.firstName).toEqual(`firstName ${i}`); + expect(items[i].profile.lastName).toEqual(`lastName ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + for (let i = 5; i < 15; i++) { + await DataStore.save( + new User({ + name: `user ${i}`, + profile: await DataStore.save( + new Profile({ + firstName: `firstName ${i}`, + lastName: `lastName ${i}`, + }) + ), + }) + ); + } + }, 100); + } catch (error) { + done(error); + } + }); + + test('attaches related belongsTo properties consistently with query() on UPDATE', async done => { + try { + const expecteds = [ + ['old post 0', 'old post 1', 'old post 2', 'old post 3', 'old post 4'], + ['new post 0', 'new post 1', 'new post 2', 'new post 3', 'new post 4'], + ]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Comment({ + content: `comment content ${i}`, + post: await DataStore.save( + new Post({ + title: `old post ${i}`, + }) + ), + }) + ); + } + + const sub = DataStore.observeQuery(Comment).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || []; + expect(items.length).toBe(expected.length); + + for (let i = 0; i < expected.length; i++) { + expect(items[i].content).toContain(`comment content ${i}`); + expect(items[i].post.title).toEqual(expected[i]); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + let postIndex = 0; + const comments = await DataStore.query(Comment); + for (const comment of comments) { + const newPost = await DataStore.save( new Post({ - title: `the post ${i}`, + title: `new post ${postIndex++}`, + }) + ); + + await DataStore.save( + Comment.copyOf(comment, draft => { + draft.content = `updated: ${comment.content}`; + draft.post = newPost; + }) + ); + } + }, 100); + } catch (error) { + done(error); + } + }); + + test('attaches related hasOne properties consistently with query() on UPDATE', async done => { + try { + const expecteds = [ + [ + 'first name 0', + 'first name 1', + 'first name 2', + 'first name 3', + 'first name 4', + ], + [ + 'new first name 0', + 'new first name 1', + 'new first name 2', + 'new first name 3', + 'new first name 4', + ], + ]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new User({ + name: `user ${i}`, + profile: await DataStore.save( + new Profile({ + firstName: `first name ${i}`, + lastName: `last name ${i}`, + }) + ), + }) + ); + } + + const sub = DataStore.observeQuery(User).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || []; + expect(items.length).toBe(expected.length); + + for (let i = 0; i < expected.length; i++) { + expect(items[i].name).toContain(`user ${i}`); + expect(items[i].profile.firstName).toEqual(expected[i]); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + let userIndex = 0; + const users = await DataStore.query(User); + for (const user of users) { + const newProfile = await DataStore.save( + new Profile({ + firstName: `new first name ${userIndex++}`, + lastName: `new last name ${userIndex}`, + }) + ); + + await DataStore.save( + User.copyOf(user, draft => { + draft.name = `updated: ${user.name}`; + draft.profile = newProfile; }) ); } @@ -412,6 +876,22 @@ describe('DataStore tests', () => { ({ initSchema, DataStore } = require('../src/datastore/datastore')); }); + test('error on schema not initialized on start', async () => { + const errorLog = jest.spyOn(console, 'error'); + const errorRegex = /Schema is not initialized/; + await expect(DataStore.start()).rejects.toThrow(errorRegex); + + expect(errorLog).toHaveBeenCalledWith(expect.stringMatching(errorRegex)); + }); + + test('error on schema not initialized on clear', async () => { + const errorLog = jest.spyOn(console, 'error'); + const errorRegex = /Schema is not initialized/; + await expect(DataStore.clear()).rejects.toThrow(errorRegex); + + expect(errorLog).toHaveBeenCalledWith(expect.stringMatching(errorRegex)); + }); + describe('initSchema tests', () => { test('Model class is created', () => { const classes = initSchema(testSchema()); @@ -638,6 +1118,77 @@ describe('DataStore tests', () => { expect(model2.optionalField1).toBeNull(); }); + test('multiple copyOf operations carry all changes on save', async () => { + let model: Model; + const save = jest.fn(() => [model]); + const query = jest.fn(() => [model]); + + jest.resetModules(); + jest.doMock('../src/storage/storage', () => { + const mock = jest.fn().mockImplementation(() => { + const _mock = { + init: jest.fn(), + save, + query, + runExclusive: jest.fn(fn => fn.bind(this, _mock)()), + }; + + return _mock; + }); + + (mock).getNamespace = () => ({ models: {} }); + + return { ExclusiveStorage: mock }; + }); + + ({ initSchema, DataStore } = require('../src/datastore/datastore')); + + const classes = initSchema(testSchema()); + + const { Model } = classes as { Model: PersistentModelConstructor }; + + const model1 = new Model({ + dateCreated: new Date().toISOString(), + field1: 'original', + optionalField1: 'original', + }); + model = model1; + + await DataStore.save(model1); + + const model2 = Model.copyOf(model1, draft => { + (draft).field1 = 'field1Change1'; + (draft).optionalField1 = 'optionalField1Change1'; + }); + + const model3 = Model.copyOf(model2, draft => { + (draft).field1 = 'field1Change2'; + }); + model = model3; + + await DataStore.save(model3); + + const [settingsSave, saveOriginalModel, saveModel3] = ( + save.mock.calls + ); + + const [_model, _condition, _mutator, [patches]] = saveModel3; + + const expectedPatches = [ + { + op: 'replace', + path: ['field1'], + value: 'field1Change2', + }, + { + op: 'replace', + path: ['optionalField1'], + value: 'optionalField1Change1', + }, + ]; + expect(patches).toMatchObject(expectedPatches); + }); + test('Non @model - Field cannot be changed', () => { const { Metadata } = initSchema(testSchema()) as { Metadata: NonModelTypeConstructor; @@ -885,9 +1436,9 @@ describe('DataStore tests', () => { const expectedPatches2 = [ { - op: 'add', - path: ['emails', 3], - value: 'joe@doe.com', + op: 'replace', + path: ['emails'], + value: ['john@doe.com', 'jane@doe.com', 'joe@doe.com', 'joe@doe.com'], }, ]; diff --git a/packages/datastore/__tests__/IndexedDBAdapter.test.ts b/packages/datastore/__tests__/IndexedDBAdapter.test.ts index 1240e98fcb9..db5dcd6440a 100644 --- a/packages/datastore/__tests__/IndexedDBAdapter.test.ts +++ b/packages/datastore/__tests__/IndexedDBAdapter.test.ts @@ -3,21 +3,47 @@ import 'fake-indexeddb/auto'; import { DataStore as DataStoreType, initSchema as initSchemaType, + syncClasses, } from '../src/datastore/datastore'; import { PersistentModelConstructor, SortDirection } from '../src/types'; -import { Model, User, Profile, Post, Comment, testSchema } from './helpers'; +import { + pause, + expectMutation, + Model, + User, + Profile, + Post, + Comment, + testSchema, +} from './helpers'; import { Predicates } from '../src/predicates'; +import { addCommonQueryTests } from './commonAdapterTests'; let initSchema: typeof initSchemaType; let DataStore: typeof DataStoreType; // using any to get access to private methods const IDBAdapter = Adapter; -async function pause(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - describe('IndexedDBAdapter tests', () => { + async function getMutations(adapter) { + await pause(250); + return await adapter.getAll('sync_MutationEvent'); + } + + async function clearOutbox(adapter) { + await pause(250); + return await adapter.delete(syncClasses['MutationEvent']); + } + + ({ initSchema, DataStore } = require('../src/datastore/datastore')); + addCommonQueryTests({ + initSchema, + DataStore, + storageAdapter: Adapter, + getMutations, + clearOutbox, + }); + describe('Query', () => { let Model: PersistentModelConstructor; let model1Id: string; @@ -29,6 +55,7 @@ describe('IndexedDBAdapter tests', () => { beforeAll(async () => { ({ initSchema, DataStore } = require('../src/datastore/datastore')); + DataStore.configure({ storageAdapter: Adapter }); const classes = initSchema(testSchema()); @@ -43,6 +70,9 @@ describe('IndexedDBAdapter tests', () => { const baseDate = new Date(); + await DataStore.start(); + await DataStore.clear(); + ({ id: model1Id } = await DataStore.save( new Model({ field1: 'field1 value 0', @@ -119,120 +149,6 @@ describe('IndexedDBAdapter tests', () => { expect(spyOnEngine).not.toHaveBeenCalled(); expect(spyOnMemory).not.toHaveBeenCalled(); }); - - it('should match fields of any non-empty value for `("ne", undefined)`', async () => { - const results = await DataStore.query(Model, m => - m.field1('ne', undefined) - ); - expect(results.length).toEqual(3); - }); - - it('should match fields of any non-empty value for `("ne", null)`', async () => { - const results = await DataStore.query(Model, m => m.field1('ne', null)); - expect(results.length).toEqual(3); - }); - - it('should NOT match fields of any non-empty value for `("eq", undefined)`', async () => { - const results = await DataStore.query(Model, m => - m.field1('eq', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("eq", null)`', async () => { - const results = await DataStore.query(Model, m => m.field1('eq', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("gt", null)`', async () => { - const results = await DataStore.query(Model, m => m.field1('gt', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("ge", null)`', async () => { - const results = await DataStore.query(Model, m => m.field1('ge', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("lt", null)`', async () => { - const results = await DataStore.query(Model, m => m.field1('lt', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("le", null)`', async () => { - const results = await DataStore.query(Model, m => m.field1('le', null)); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("gt", undefined)`', async () => { - const results = await DataStore.query(Model, m => - m.field1('gt', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("ge", undefined)`', async () => { - const results = await DataStore.query(Model, m => - m.field1('ge', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("lt", undefined)`', async () => { - const results = await DataStore.query(Model, m => - m.field1('lt', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should NOT match fields of any non-empty value for `("le", undefined)`', async () => { - const results = await DataStore.query(Model, m => - m.field1('le', undefined) - ); - expect(results.length).toEqual(0); - }); - - it('should match gt', async () => { - const results = await DataStore.query(Model, m => - m.field1('gt', 'field1 value 0') - ); - expect(results.length).toEqual(2); - }); - - it('should match ge', async () => { - const results = await DataStore.query(Model, m => - m.field1('ge', 'field1 value 1') - ); - expect(results.length).toEqual(2); - }); - - it('should match lt', async () => { - const results = await DataStore.query(Model, m => - m.field1('lt', 'field1 value 2') - ); - expect(results.length).toEqual(2); - }); - - it('should match le', async () => { - const results = await DataStore.query(Model, m => - m.field1('le', 'field1 value 1') - ); - expect(results.length).toEqual(2); - }); - - it('should match eq', async () => { - const results = await DataStore.query(Model, m => - m.field1('eq', 'field1 value 1') - ); - expect(results.length).toEqual(1); - }); - - it('should match ne', async () => { - const results = await DataStore.query(Model, m => - m.field1('ne', 'field1 value 1') - ); - expect(results.length).toEqual(2); - }); }); describe('Delete', () => { @@ -281,124 +197,4 @@ describe('IndexedDBAdapter tests', () => { expect(profile).toBeUndefined; }); }); - - describe('Save', () => { - let User: PersistentModelConstructor; - let Profile: PersistentModelConstructor; - let Comment: PersistentModelConstructor; - let Post: PersistentModelConstructor; - let adapter: any; - - async function getMutations() { - await pause(250); - return await adapter.getAll('sync_MutationEvent'); - } - - beforeEach(async () => { - ({ initSchema, DataStore } = require('../src/datastore/datastore')); - - DataStore.configure({ - storageAdapter: Adapter, - }); - (DataStore as any).amplifyConfig.aws_appsync_graphqlEndpoint = - 'https://0.0.0.0/does/not/exist/graphql'; - - const classes = initSchema(testSchema()); - - ({ User, Profile, Comment, Post } = classes as { - User: PersistentModelConstructor; - Profile: PersistentModelConstructor; - Comment: PersistentModelConstructor; - Post: PersistentModelConstructor; - }); - - await DataStore.clear(); - - // ensure `.storageAdapter` is set. - await DataStore.start(); - - adapter = (DataStore as any).storageAdapter; - const syncEngine = (DataStore as any).sync; - - // my jest spy-fu wasn't up to snuff here. but, this succesfully - // prevents the mutation process from clearing the mutation queue, which - // allows us to observe the state of mutations. - (syncEngine as any).mutationsProcessor.isReady = () => false; - }); - - it('should allow linking model via model field', async () => { - const profile = await DataStore.save( - new Profile({ firstName: 'Rick', lastName: 'Bob' }) - ); - - const savedUser = await DataStore.save( - new User({ name: 'test', profile }) - ); - const user1Id = savedUser.id; - - const user = await DataStore.query(User, user1Id); - expect(user.profileID).toEqual(profile.id); - expect(user.profile).toEqual(profile); - }); - - it('should allow linking model via FK', async () => { - const profile = await DataStore.save( - new Profile({ firstName: 'Rick', lastName: 'Bob' }) - ); - - const savedUser = await DataStore.save( - new User({ name: 'test', profileID: profile.id }) - ); - const user1Id = savedUser.id; - - const user = await DataStore.query(User, user1Id); - expect(user.profileID).toEqual(profile.id); - expect(user.profile).toEqual(profile); - }); - - it('should produce a single mutation for an updated model with a BelongTo (regression test)', async () => { - // SQLite adapter, for example, was producing an extra mutation - // in this scenario. - - const post = await DataStore.save( - new Post({ - title: 'some post', - }) - ); - - const comment = await DataStore.save( - new Comment({ - content: 'some comment', - post, - }) - ); - - const updatedComment = await DataStore.save( - Comment.copyOf(comment, draft => { - draft.content = 'updated content'; - }) - ); - - const mutations = await getMutations(); - - // comment update should be smashed to together with post - expect(mutations.length).toBe(2); - }); - - it('should produce a mutation for a nested BELONGS_TO insert', async () => { - const comment = await DataStore.save( - new Comment({ - content: 'newly created comment', - post: new Post({ - title: 'newly created post', - }), - }) - ); - - const mutations = await getMutations(); - - // one for the new comment, one for the new post - expect(mutations.length).toBe(2); - }); - }); }); diff --git a/packages/datastore/__tests__/authStrategies.test.ts b/packages/datastore/__tests__/authStrategies.test.ts index 39213b5659b..b08f9ba1727 100644 --- a/packages/datastore/__tests__/authStrategies.test.ts +++ b/packages/datastore/__tests__/authStrategies.test.ts @@ -449,9 +449,11 @@ async function testMultiAuthStrategy({ }) { mockCurrentUser({ hasAuthenticatedUser }); - const multiAuthStrategy = + const multiAuthStrategyWrapper = require('../src/authModeStrategies/multiAuthStrategy').multiAuthStrategy; + const multiAuthStrategy = multiAuthStrategyWrapper({}); + const schema = getAuthSchema(authRules); const authModes = await multiAuthStrategy({ diff --git a/packages/datastore/__tests__/commonAdapterTests.ts b/packages/datastore/__tests__/commonAdapterTests.ts new file mode 100644 index 00000000000..0cf6cca5099 --- /dev/null +++ b/packages/datastore/__tests__/commonAdapterTests.ts @@ -0,0 +1,370 @@ +import { + DataStore as DataStoreType, + PersistentModelConstructor, + initSchema as initSchemaType, +} from '../src/'; + +import { + pause, + expectMutation, + Model, + User, + Profile, + Post, + Comment, + testSchema, +} from './helpers'; + +export { pause }; + +/** + * Adds common query test cases that all adapters should support. + * + * @param ctx A context object that provides a DataStore property, which returns + * a DataStore instance loaded with the storage adapter to test. + */ +export function addCommonQueryTests({ + initSchema, + DataStore, + storageAdapter, + getMutations, + clearOutbox, +}) { + describe('Common `query()` cases', () => { + let Model: PersistentModelConstructor; + let Comment: PersistentModelConstructor; + let Post: PersistentModelConstructor; + + /** + * Creates the given number of models, with `field1` populated to + * `field1 value ${i}`. + * + * @param qty number of models to create. (default 3) + */ + async function addModels(qty = 3) { + // NOTE: sort() test on these models can be flaky unless we + // strictly control the datestring of each! In a non-negligible percentage + // of test runs on a reasonably fast machine, DataStore.save() seemed to return + // quickly enough that dates were colliding. (or so it seemed!) + const baseDate = new Date(); + + for (let i = 0; i < qty; i++) { + await DataStore.save( + new Model({ + field1: `field1 value ${i}`, + dateCreated: new Date(baseDate.getTime() + i).toISOString(), + emails: [`field${i}@example.com`], + }) + ); + } + } + + beforeEach(async () => { + DataStore.configure({ storageAdapter }); + + // establishing a fake appsync endpoint tricks DataStore into attempting + // sync operations, which we'll leverage to monitor how DataStore manages + // the outbox. + (DataStore as any).amplifyConfig.aws_appsync_graphqlEndpoint = + 'https://0.0.0.0/does/not/exist/graphql'; + + const classes = initSchema(testSchema()); + ({ Comment, Model, Post } = classes as { + Comment: PersistentModelConstructor; + Model: PersistentModelConstructor; + Post: PersistentModelConstructor; + }); + await DataStore.clear(); + + // start() ensures storageAdapter is set + await DataStore.start(); + + const adapter = (DataStore as any).storageAdapter; + const db = (adapter as any).db; + const syncEngine = (DataStore as any).sync; + + // my jest spy-fu wasn't up to snuff here. but, this succesfully + // prevents the mutation process from clearing the mutation queue, which + // allows us to observe the state of mutations. + (syncEngine as any).mutationsProcessor.isReady = () => false; + + await addModels(3); + }); + + afterAll(async () => { + await DataStore.clear(); + + // prevent cross-contamination with other test suites that are not ~literally~ + // expecting sync call counts to be ZERO. + (DataStore as any).amplifyConfig.aws_appsync_graphqlEndpoint = ''; + }); + + it('should match fields of any non-empty value for `("ne", undefined)`', async () => { + const results = await DataStore.query(Model, m => + m.field1('ne', undefined) + ); + expect(results.length).toEqual(3); + }); + + it('should match fields of any non-empty value for `("ne", null)`', async () => { + const results = await DataStore.query(Model, m => m.field1('ne', null)); + expect(results.length).toEqual(3); + }); + + it('should NOT match fields of any non-empty value for `("eq", undefined)`', async () => { + const results = await DataStore.query(Model, m => + m.field1('eq', undefined) + ); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("eq", null)`', async () => { + const results = await DataStore.query(Model, m => m.field1('eq', null)); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("gt", null)`', async () => { + const results = await DataStore.query(Model, m => m.field1('gt', null)); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("ge", null)`', async () => { + const results = await DataStore.query(Model, m => m.field1('ge', null)); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("lt", null)`', async () => { + const results = await DataStore.query(Model, m => m.field1('lt', null)); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("le", null)`', async () => { + const results = await DataStore.query(Model, m => m.field1('le', null)); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("gt", undefined)`', async () => { + const results = await DataStore.query(Model, m => + m.field1('gt', undefined) + ); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("ge", undefined)`', async () => { + const results = await DataStore.query(Model, m => + m.field1('ge', undefined) + ); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("lt", undefined)`', async () => { + const results = await DataStore.query(Model, m => + m.field1('lt', undefined) + ); + expect(results.length).toEqual(0); + }); + + it('should NOT match fields of any non-empty value for `("le", undefined)`', async () => { + const results = await DataStore.query(Model, m => + m.field1('le', undefined) + ); + expect(results.length).toEqual(0); + }); + + it('should match gt', async () => { + const results = await DataStore.query(Model, m => + m.field1('gt', 'field1 value 0') + ); + expect(results.length).toEqual(2); + }); + + it('should match ge', async () => { + const results = await DataStore.query(Model, m => + m.field1('ge', 'field1 value 1') + ); + expect(results.length).toEqual(2); + }); + + it('should match lt', async () => { + const results = await DataStore.query(Model, m => + m.field1('lt', 'field1 value 2') + ); + expect(results.length).toEqual(2); + }); + + it('should match le', async () => { + const results = await DataStore.query(Model, m => + m.field1('le', 'field1 value 1') + ); + expect(results.length).toEqual(2); + }); + + it('should match eq', async () => { + const results = await DataStore.query(Model, m => + m.field1('eq', 'field1 value 1') + ); + expect(results.length).toEqual(1); + }); + + it('should match ne', async () => { + const results = await DataStore.query(Model, m => + m.field1('ne', 'field1 value 1') + ); + expect(results.length).toEqual(2); + }); + }); + + describe('Common `save()` cases', () => { + let Comment: PersistentModelConstructor; + let Post: PersistentModelConstructor; + let Profile: PersistentModelConstructor; + let User: PersistentModelConstructor; + let adapter: any; + + beforeEach(async () => { + DataStore.configure({ storageAdapter }); + + // establishing a fake appsync endpoint tricks DataStore into attempting + // sync operations, which we'll leverage to monitor how DataStore manages + // the outbox. + (DataStore as any).amplifyConfig.aws_appsync_graphqlEndpoint = + 'https://0.0.0.0/does/not/exist/graphql'; + + const classes = initSchema(testSchema()); + ({ User, Profile, Comment, Post } = classes as { + Comment: PersistentModelConstructor; + Model: PersistentModelConstructor; + Post: PersistentModelConstructor; + Profile: PersistentModelConstructor; + User: PersistentModelConstructor; + }); + await DataStore.clear(); + + // start() ensures storageAdapter is set + await DataStore.start(); + + adapter = (DataStore as any).storageAdapter; + const db = (adapter as any).db; + const syncEngine = (DataStore as any).sync; + + // my jest spy-fu wasn't up to snuff here. but, this succesfully + // prevents the mutation process from clearing the mutation queue, which + // allows us to observe the state of mutations. + (syncEngine as any).mutationsProcessor.isReady = () => false; + }); + + afterAll(async () => { + await DataStore.clear(); + (DataStore as any).amplifyConfig.aws_appsync_graphqlEndpoint = ''; + }); + + it('should allow linking model via model field', async () => { + const profile = await DataStore.save( + new Profile({ firstName: 'Rick', lastName: 'Bob' }) + ); + + const savedUser = await DataStore.save( + new User({ name: 'test', profile }) + ); + const user1Id = savedUser.id; + + const user = await DataStore.query(User, user1Id); + expect(user.profileID).toEqual(profile.id); + expect(user.profile).toEqual(profile); + }); + + it('should allow linking model via FK', async () => { + const profile = await DataStore.save( + new Profile({ firstName: 'Rick', lastName: 'Bob' }) + ); + + const savedUser = await DataStore.save( + new User({ name: 'test', profileID: profile.id }) + ); + const user1Id = savedUser.id; + + const user = await DataStore.query(User, user1Id); + expect(user.profileID).toEqual(profile.id); + expect(user.profile).toEqual(profile); + }); + + it('should produce a single mutation for an updated model with a BelongTo (regression test)', async () => { + // SQLite adapter, for example, was producing an extra mutation + // in this scenario. + + const post = await DataStore.save( + new Post({ + title: 'some post', + }) + ); + + const comment = await DataStore.save( + new Comment({ + content: 'some comment', + post, + }) + ); + + const updatedComment = await DataStore.save( + Comment.copyOf(comment, draft => { + draft.content = 'updated content'; + }) + ); + + const mutations = await getMutations(adapter); + + // comment update should be smashed to together with post + expect(mutations.length).toBe(2); + expectMutation(mutations[0], { title: 'some post' }); + expectMutation(mutations[1], { + content: 'updated content', + postId: mutations[0].modelId, + }); + }); + + it('should produce a mutation for a nested BELONGS_TO insert', async () => { + const comment = await DataStore.save( + new Comment({ + content: 'newly created comment', + post: new Post({ + title: 'newly created post', + }), + }) + ); + + const mutations = await getMutations(adapter); + + // one for the new comment, one for the new post + expect(mutations.length).toBe(2); + expectMutation(mutations[0], { title: 'newly created post' }); + expectMutation(mutations[1], { + content: 'newly created comment', + postId: mutations[0].modelId, + }); + }); + + it('only includes changed fields in mutations', async () => { + const profile = await DataStore.save( + new Profile({ firstName: 'original first', lastName: 'original last' }) + ); + + await clearOutbox(adapter); + + await DataStore.save( + Profile.copyOf(profile, draft => { + draft.firstName = 'new first'; + }) + ); + + const mutations = await getMutations(adapter); + + expect(mutations.length).toBe(1); + expectMutation(mutations[0], { + firstName: 'new first', + _version: v => v === undefined || v === null, + _lastChangedAt: v => v === undefined || v === null, + _deleted: v => v === undefined || v === null, + }); + }); + }); +} diff --git a/packages/datastore/__tests__/helpers.ts b/packages/datastore/__tests__/helpers.ts index 752ac56a86f..d237cc7f53c 100644 --- a/packages/datastore/__tests__/helpers.ts +++ b/packages/datastore/__tests__/helpers.ts @@ -6,6 +6,89 @@ import { SchemaModel, } from '../src/types'; +/** + * Convenience function to wait for a number of ms. + * + * Intended as a cheap way to wait for async operations to settle. + * + * @param ms number of ms to pause for + */ +export async function pause(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Case insensitive regex that matches GUID's and UUID's. + * It does NOT permit whitespace on either end of the string. The caller must `trim()` first as-needed. + */ +export const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Tests a mutation for expected values. If values are present on the mutation + * that are not expected, throws an error. Expected values can be listed as a + * literal value, a regular expression, or a function (v => bool). + * + * `id` is automatically tested and expected to be a UUID unless an alternative + * matcher is provided. + * + * @param mutation A mutation record to check + * @param values An object for specific values to test. Format of key: value | regex | v => bool + */ +export function expectMutation(mutation, values) { + const data = JSON.parse(mutation.data); + const matchers = { + id: UUID_REGEX, + ...values, + }; + const errors = [ + ...errorsFrom(data, matchers), + ...extraFieldsFrom(data, matchers).map(f => `Unexpected field: ${f}`), + ]; + if (errors.length > 0) { + throw new Error( + `Bad mutation: ${JSON.stringify(data, null, 2)}\n${errors.join('\n')}` + ); + } +} + +/** + * Checks an object for adherence to expected values from a set of matchers. + * Returns a list of erroneous key-value pairs. + * @param data the object to validate. + * @param matchers the matcher functions/values/regexes to test the object with + */ +export function errorsFrom(data, matchers) { + return Object.entries(matchers).reduce((errors, [property, matcher]) => { + const value = data[property]; + if ( + !( + (typeof matcher === 'function' && matcher(value)) || + (matcher instanceof RegExp && matcher.test(value)) || + value === matcher + ) + ) { + errors.push( + `Property '${property}' value "${value}" does not match "${matcher}"` + ); + } + return errors; + }, []); +} + +/** + * Checks to see if a given object contains any extra, unexpected properties. + * If any are present, it returns the list of unexpectd fields. + * + * @param data the object that MIGHT contain extra fields. + * @param template the authorative template object. + */ +export function extraFieldsFrom(data, template) { + const fields = Object.keys(data); + const expectedFields = new Set(Object.keys(template)); + return fields.filter(name => !expectedFields.has(name)); +} + export declare class Model { public readonly id: string; public readonly field1: string; @@ -911,3 +994,13 @@ export function internalTestSchema(): InternalSchema { version: '1', }; } + +export function smallTestSchema(): Schema { + const schema = testSchema(); + return { + ...schema, + models: { + Model: schema.models.Model, + }, + }; +} diff --git a/packages/datastore/__tests__/mutation.test.ts b/packages/datastore/__tests__/mutation.test.ts index 5ac5b3a3462..2795d052de6 100644 --- a/packages/datastore/__tests__/mutation.test.ts +++ b/packages/datastore/__tests__/mutation.test.ts @@ -1,3 +1,4 @@ +const mockRestPost = jest.fn(); import { MutationProcessor, safeJitteredBackoff, @@ -16,14 +17,21 @@ import { } from '../src/types'; import { createMutationInstanceFromModelOperation } from '../src/sync/utils'; import { MutationEvent } from '../src/sync/'; +import { Constants } from '@aws-amplify/core'; +import { USER_AGENT_SUFFIX_DATASTORE } from '../src/util'; let syncClasses: any; let modelInstanceCreator: any; let Model: PersistentModelConstructor; let PostCustomPK: PersistentModelConstructor; let PostCustomPKSort: PersistentModelConstructor; +let axiosError; -describe('Jittered retry', () => { +beforeEach(() => { + axiosError = timeoutError; +}); + +describe('Jittered backoff', () => { it('should progress exponentially until some limit', () => { const COUNT = 13; @@ -141,11 +149,103 @@ describe('MutationProcessor', () => { expect(input.postId).toEqual(100); }); }); + describe('Call to rest api', () => { + it('Should send a user agent with the datastore suffix the rest api request', async () => { + jest.spyOn(mutationProcessor, 'resume'); + await mutationProcessor.resume(); + + expect(mockRestPost).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-amz-user-agent': `${Constants.userAgent}${USER_AGENT_SUFFIX_DATASTORE}`, + }), + }) + ); + }); + }); afterAll(() => { jest.restoreAllMocks(); }); }); +describe('error handler', () => { + let mutationProcessor: MutationProcessor; + const errorHandler = jest.fn(); + + beforeEach(async () => { + errorHandler.mockClear(); + mutationProcessor = await instantiateMutationProcessor({ errorHandler }); + }); + + test('newly required field', async () => { + axiosError = { + message: "Variable 'name' has coerced Null value for NonNull type", + name: 'Error', + code: '', + errorType: '', + }; + await mutationProcessor.resume(); + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + operation: 'Create', + process: 'mutate', + errorType: 'BadRecord', + }) + ); + }); + + test('connection timout', async () => { + axiosError = { + message: 'Connection failed: Connection Timeout', + name: 'Error', + code: '', + errorType: '', + }; + await mutationProcessor.resume(); + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + operation: 'Create', + process: 'mutate', + errorType: 'Transient', + }) + ); + }); + + test('server error', async () => { + axiosError = { + message: 'Error: Request failed with status code 500', + name: 'Error', + code: '', + errorType: '', + }; + await mutationProcessor.resume(); + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + operation: 'Create', + process: 'mutate', + errorType: 'Transient', + }) + ); + }); + + test('no auth decorator', async () => { + axiosError = { + message: 'Request failed with status code 401', + name: 'Error', + code: '', + errorType: '', + }; + await mutationProcessor.resume(); + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + operation: 'Create', + process: 'mutate', + errorType: 'Unauthorized', + }) + ); + }); +}); // Mocking restClient.post to throw the error we expect // when experiencing poor network conditions jest.mock('@aws-amplify/api-rest', () => { @@ -153,7 +253,7 @@ jest.mock('@aws-amplify/api-rest', () => { ...jest.requireActual('@aws-amplify/api-rest'), RestClient() { return { - post: jest.fn().mockImplementation(() => { + post: mockRestPost.mockImplementation(() => { return Promise.reject(axiosError); }), getCancellableToken: () => {}, @@ -181,6 +281,7 @@ jest.mock('@aws-amplify/api', () => { graphqlInstance.configure(awsconfig); return { + ...jest.requireActual('@aws-amplify/api'), graphql: graphqlInstance.graphql.bind(graphqlInstance), }; }); @@ -202,7 +303,9 @@ jest.mock('@aws-amplify/core', () => { // Mocking just enough dependencies for us to be able to // instantiate a working MutationProcessor // includes functional mocked outbox containing a single MutationEvent -async function instantiateMutationProcessor() { +async function instantiateMutationProcessor({ + errorHandler = () => null, +} = {}) { let schema: InternalSchema = internalTestSchema(); jest.doMock('../src/sync/', () => ({ @@ -266,7 +369,10 @@ async function instantiateMutationProcessor() { aws_appsync_authenticationType: 'API_KEY', aws_appsync_apiKey: 'da2-xxxxxxxxxxxxxxxxxxxxxx', }, - () => null + () => null, + errorHandler, + () => null as any, + {} as any ); (mutationProcessor as any).observer = true; @@ -296,7 +402,7 @@ async function createMutationEvent(model, opType): Promise { } // expected error when experiencing 100% packet loss -const axiosError = { +const timeoutError = { message: 'timeout of 0ms exceeded', name: 'Error', stack: diff --git a/packages/datastore/__tests__/subscription.test.ts b/packages/datastore/__tests__/subscription.test.ts index 3f5b3531c2e..a06eacb03d1 100644 --- a/packages/datastore/__tests__/subscription.test.ts +++ b/packages/datastore/__tests__/subscription.test.ts @@ -1,9 +1,33 @@ +import Observable from 'zen-observable-ts'; +let mockObservable = new Observable(() => {}); +const mockGraphQL = jest.fn(() => mockObservable); + +import Amplify from 'aws-amplify'; import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api'; +import { CONTROL_MSG as PUBSUB_CONTROL_MSG } from '@aws-amplify/pubsub'; import { SubscriptionProcessor, USER_CREDENTIALS, } from '../src/sync/processors/subscription'; -import { SchemaModel } from '../src/types'; +import { + internalTestSchema, + Model as ModelType, + smallTestSchema, +} from './helpers'; +import { + SchemaModel, + InternalSchema, + PersistentModelConstructor, +} from '../src/types'; +import { USER_AGENT_SUFFIX_DATASTORE } from '../src/util'; + +// mock graphql to return a mockable observable +jest.mock('@aws-amplify/api', () => { + return { + ...jest.requireActual('@aws-amplify/api'), + graphql: mockGraphQL, + }; +}); describe('sync engine subscription module', () => { test('owner authorization', () => { @@ -566,6 +590,123 @@ describe('sync engine subscription module', () => { }); }); +describe('error handler', () => { + let Model: PersistentModelConstructor; + + let subscriptionProcessor: SubscriptionProcessor; + const errorHandler = jest.fn(); + beforeEach(async () => { + errorHandler.mockClear(); + subscriptionProcessor = await instantiateSubscriptionProcessor({ + errorHandler, + }); + }); + + test('error handler once after all retires have failed', done => { + Amplify.Logger.LOG_LEVEL = 'DEBUG'; + const debugLog = jest.spyOn(console, 'log'); + const message = PUBSUB_CONTROL_MSG.REALTIME_SUBSCRIPTION_INIT_ERROR; + mockObservable = new Observable(observer => { + observer.error({ + error: { + errors: [ + { + message, + }, + ], + }, + }); + }); + + const subscription = subscriptionProcessor.start(); + subscription[0].subscribe({ + error: data => { + console.log(data); + console.log(errorHandler.mock.calls); + + // call once each for Create, Update, and Delete + expect(errorHandler).toHaveBeenCalledTimes(3); + ['Create', 'Update', 'Delete'].forEach(operation => { + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + process: 'subscribe', + errorType: 'Unknown', + message, + model: 'Model', + operation, + }) + ); + // expect logger.debug to be called 6 times for auth mode (2 for each operation) + // can't use toHaveBeenCalledTimes because it is called elsewhere unrelated to the test + expect(debugLog).toHaveBeenCalledWith( + expect.stringMatching( + new RegExp( + `[DEBUG].*${operation} subscription failed with authMode: API_KEY` + ) + ) + ); + expect(debugLog).toHaveBeenCalledWith( + expect.stringMatching( + new RegExp( + `[DEBUG].*${operation} subscription failed with authMode: AMAZON_COGNITO_USER_POOLS` + ) + ) + ); + + expect(mockGraphQL).toHaveBeenCalledWith( + expect.objectContaining({ + userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, + }) + ); + }); + + done(); + }, + }); + }, 500); + + async function instantiateSubscriptionProcessor({ + errorHandler = () => null, + }) { + let schema: InternalSchema = internalTestSchema(); + + const { initSchema, DataStore } = require('../src/datastore/datastore'); + const classes = initSchema(smallTestSchema()); + + ({ Model } = classes as { + Model: PersistentModelConstructor; + }); + + const userClasses = { + Model, + }; + + await DataStore.start(); + ({ schema } = (DataStore as any).storage.storage); + const syncPredicates = new WeakMap(); + + const subscriptionProcessor = new SubscriptionProcessor( + schema, + syncPredicates, + { + aws_project_region: 'us-west-2', + aws_appsync_graphqlEndpoint: + 'https://xxxxxxxxxxxxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com/graphql', + aws_appsync_region: 'us-west-2', + aws_appsync_authenticationType: 'API_KEY', + aws_appsync_apiKey: 'da2-xxxxxxxxxxxxxxxxxxxxxx', + }, + () => [ + GRAPHQL_AUTH_MODE.API_KEY, + GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, + ], + errorHandler + ); + + return subscriptionProcessor; + } +}); + const accessTokenPayload = { sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', 'cognito:groups': ['mygroup'], diff --git a/packages/datastore/__tests__/sync.test.ts b/packages/datastore/__tests__/sync.test.ts index 82c5ea65e1a..4e5def45a31 100644 --- a/packages/datastore/__tests__/sync.test.ts +++ b/packages/datastore/__tests__/sync.test.ts @@ -1,7 +1,8 @@ // These tests should be replaced once SyncEngine.partialDataFeatureFlagEnabled is removed. - import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api-graphql'; import { defaultAuthStrategy } from '../src/authModeStrategies'; +import { USER_AGENT_SUFFIX_DATASTORE } from '../src/util'; +let mockGraphQl; const sessionStorageMock = (() => { let store = {}; @@ -25,10 +26,7 @@ const sessionStorageMock = (() => { Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock, }); - -describe('Sync', () => { - describe('jitteredRetry', () => { - const defaultQuery = `query { +const defaultQuery = `query { syncPosts { items { id @@ -42,11 +40,13 @@ describe('Sync', () => { startedAt } }`; - const defaultVariables = {}; - const defaultOpName = 'syncPosts'; - const defaultModelDefinition = { name: 'Post' }; - const defaultAuthMode = GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS; +const defaultVariables = {}; +const defaultOpName = 'syncPosts'; +const defaultModelDefinition = { name: 'Post' }; +const defaultAuthMode = GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS; +describe('Sync', () => { + describe('jitteredRetry', () => { beforeEach(() => { window.sessionStorage.clear(); jest.resetModules(); @@ -282,6 +282,157 @@ describe('Sync', () => { } }); }); + + it('should send user agent suffix with graphql request', async () => { + window.sessionStorage.setItem('datastorePartialData', 'true'); + const resolveResponse = { + data: { + syncPosts: { + items: [ + { + id: '1', + title: 'Item 1', + }, + { + id: '2', + title: 'Item 2', + }, + ], + }, + }, + }; + + const SyncProcessor = jitteredRetrySyncProcessorSetup({ + resolveResponse, + }); + + await SyncProcessor.jitteredRetry({ + query: defaultQuery, + variables: defaultVariables, + opName: defaultOpName, + modelDefinition: defaultModelDefinition, + }); + + expect(mockGraphQl).toHaveBeenCalledWith( + expect.objectContaining({ + userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, + }) + ); + }); + }); + + describe('error handler', () => { + const errorHandler = jest.fn(); + const data = { + syncPosts: { + items: [ + { + id: '1', + title: 'Item 1', + }, + null, + { + id: '3', + title: 'Item 3', + }, + ], + }, + }; + + beforeEach(async () => { + window.sessionStorage.clear(); + jest.resetModules(); + jest.resetAllMocks(); + errorHandler.mockClear(); + window.sessionStorage.setItem('datastorePartialData', 'true'); + }); + + test('bad record', async () => { + const syncProcessor = jitteredRetrySyncProcessorSetup({ + errorHandler, + rejectResponse: { + data, + errors: [ + { + message: 'Cannot return boolean for string type', + }, + ], + }, + }); + + await syncProcessor.jitteredRetry({ + query: defaultQuery, + variables: defaultVariables, + opName: defaultOpName, + modelDefinition: defaultModelDefinition, + }); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + operation: 'syncPosts', + process: 'sync', + errorType: 'BadRecord', + }) + ); + }); + + test('connection timeout', async () => { + const syncProcessor = jitteredRetrySyncProcessorSetup({ + errorHandler, + rejectResponse: { + data, + errors: [ + { + message: 'Connection failed: Connection Timeout', + }, + ], + }, + }); + + await syncProcessor.jitteredRetry({ + query: defaultQuery, + variables: defaultVariables, + opName: defaultOpName, + modelDefinition: defaultModelDefinition, + }); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + operation: 'syncPosts', + process: 'sync', + errorType: 'Transient', + }) + ); + }); + + test('server error', async () => { + const syncProcessor = jitteredRetrySyncProcessorSetup({ + errorHandler, + rejectResponse: { + data, + errors: [ + { + message: 'Error: Request failed with status code 500', + }, + ], + }, + }); + + await syncProcessor.jitteredRetry({ + query: defaultQuery, + variables: defaultVariables, + opName: defaultOpName, + modelDefinition: defaultModelDefinition, + }); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + operation: 'syncPosts', + process: 'sync', + errorType: 'Transient', + }) + ); + }); }); }); @@ -289,21 +440,26 @@ function jitteredRetrySyncProcessorSetup({ rejectResponse, resolveResponse, coreMocks, + errorHandler = () => null, }: { rejectResponse?: any; resolveResponse?: any; coreMocks?: object; + errorHandler?: () => null; }) { - jest.mock('@aws-amplify/api', () => ({ - ...jest.requireActual('@aws-amplify/api'), - graphql: () => + mockGraphQl = jest.fn( + () => new Promise((res, rej) => { if (resolveResponse) { res(resolveResponse); } else if (rejectResponse) { rej(rejectResponse); } - }), + }) + ); + jest.mock('@aws-amplify/api', () => ({ + ...jest.requireActual('@aws-amplify/api'), + graphql: mockGraphQl, })); jest.mock('@aws-amplify/core', () => ({ @@ -326,7 +482,9 @@ function jitteredRetrySyncProcessorSetup({ testInternalSchema, null, // syncPredicates { aws_appsync_authenticationType: 'userPools' }, - defaultAuthStrategy + defaultAuthStrategy, + errorHandler, + {} ); return SyncProcessor; diff --git a/packages/datastore/__tests__/util.test.ts b/packages/datastore/__tests__/util.test.ts index 1a5ee851f4d..97fc65966da 100644 --- a/packages/datastore/__tests__/util.test.ts +++ b/packages/datastore/__tests__/util.test.ts @@ -1,3 +1,4 @@ +import { enablePatches, produce, Patch } from 'immer'; import { isAWSDate, isAWSDateTime, @@ -11,6 +12,7 @@ import { validatePredicateField, valuesEqual, processCompositeKeys, + mergePatches, } from '../src/util'; describe('datastore util', () => { @@ -592,4 +594,128 @@ describe('datastore util', () => { expect(isAWSIPAddress(test)).toBe(false); }); }); + + describe('mergePatches', () => { + enablePatches(); + test('merge patches with no conflict', () => { + const modelA = { + foo: 'originalFoo', + bar: 'originalBar', + }; + let patchesAB; + let patchesBC; + const modelB = produce( + modelA, + draft => { + draft.foo = 'newFoo'; + }, + patches => { + patchesAB = patches; + } + ); + const modelC = produce( + modelB, + draft => { + draft.bar = 'newBar'; + }, + patches => { + patchesBC = patches; + } + ); + + const mergedPatches = mergePatches(modelA, patchesAB, patchesBC); + expect(mergedPatches).toEqual([ + { + op: 'replace', + path: ['foo'], + value: 'newFoo', + }, + { + op: 'replace', + path: ['bar'], + value: 'newBar', + }, + ]); + }); + test('merge patches with conflict', () => { + const modelA = { + foo: 'originalFoo', + bar: 'originalBar', + }; + let patchesAB; + let patchesBC; + const modelB = produce( + modelA, + draft => { + draft.foo = 'newFoo'; + draft.bar = 'newBar'; + }, + patches => { + patchesAB = patches; + } + ); + const modelC = produce( + modelB, + draft => { + draft.bar = 'newestBar'; + }, + patches => { + patchesBC = patches; + } + ); + + const mergedPatches = mergePatches(modelA, patchesAB, patchesBC); + expect(mergedPatches).toEqual([ + { + op: 'replace', + path: ['foo'], + value: 'newFoo', + }, + { + op: 'replace', + path: ['bar'], + value: 'newestBar', + }, + ]); + }); + test('merge patches with conflict - list', () => { + const modelA = { + foo: [1, 2, 3], + }; + let patchesAB; + let patchesBC; + const modelB = produce( + modelA, + draft => { + draft.foo.push(4); + }, + patches => { + patchesAB = patches; + } + ); + const modelC = produce( + modelB, + draft => { + draft.foo.push(5); + }, + patches => { + patchesBC = patches; + } + ); + + const mergedPatches = mergePatches(modelA, patchesAB, patchesBC); + expect(mergedPatches).toEqual([ + { + op: 'add', + path: ['foo', 3], + value: 4, + }, + { + op: 'add', + path: ['foo', 4], + value: 5, + }, + ]); + }); + }); }); diff --git a/packages/datastore/package.json b/packages/datastore/package.json index 4ff8fd6814d..a2f594b3227 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/datastore", - "version": "3.10.0", + "version": "3.12.8", "description": "AppSyncLocal support for aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -43,16 +43,16 @@ "devDependencies": { "@react-native-community/netinfo": "4.7.0", "@types/uuid": "3.4.5", - "dexie": "3.2.0", + "dexie": "3.2.2", "dexie-export-import": "1.0.3", "fake-indexeddb": "3.0.0" }, "dependencies": { - "@aws-amplify/api": "4.0.38", - "@aws-amplify/auth": "4.5.2", - "@aws-amplify/core": "4.5.2", - "@aws-amplify/pubsub": "4.3.2", - "amazon-cognito-identity-js": "5.2.8", + "@aws-amplify/api": "4.0.51", + "@aws-amplify/auth": "4.6.4", + "@aws-amplify/core": "4.7.2", + "@aws-amplify/pubsub": "4.5.1", + "amazon-cognito-identity-js": "5.2.10", "idb": "5.0.6", "immer": "9.0.6", "ulid": "2.3.0", @@ -85,7 +85,8 @@ "testPathIgnorePatterns": [ "__tests__/model.ts", "__tests__/schema.ts", - "__tests__/helpers.ts" + "__tests__/helpers.ts", + "__tests__/commonAdapterTests.ts" ], "moduleFileExtensions": [ "ts", diff --git a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts index 0d601daa161..9e081543c07 100644 --- a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts +++ b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts @@ -5,6 +5,7 @@ import { ModelAttributeAuthProperty, ModelAttributeAuthProvider, ModelAttributeAuthAllow, + AmplifyContext, } from '../types'; function getProviderFromRule( @@ -121,29 +122,31 @@ function getAuthRules({ return Array.from(authModes); } -export const multiAuthStrategy: AuthModeStrategy = async ({ - schema, - modelName, -}) => { - let currentUser; - try { - currentUser = await Auth.currentAuthenticatedUser(); - } catch (e) { - // No current user - } +export const multiAuthStrategy: ( + amplifyContext: AmplifyContext +) => AuthModeStrategy = + (amplifyContext: AmplifyContext) => + async ({ schema, modelName }) => { + amplifyContext.Auth = amplifyContext.Auth || Auth; + let currentUser; + try { + currentUser = await amplifyContext.Auth.currentAuthenticatedUser(); + } catch (e) { + // No current user + } - const { attributes } = schema.namespaces.user.models[modelName]; + const { attributes } = schema.namespaces.user.models[modelName]; - if (attributes) { - const authAttribute = attributes.find(attr => attr.type === 'auth'); + if (attributes) { + const authAttribute = attributes.find(attr => attr.type === 'auth'); - if (authAttribute.properties && authAttribute.properties.rules) { - const sortedRules = sortAuthRulesWithPriority( - authAttribute.properties.rules - ); + if (authAttribute?.properties?.rules) { + const sortedRules = sortAuthRulesWithPriority( + authAttribute.properties.rules + ); - return getAuthRules({ currentUser, rules: sortedRules }); + return getAuthRules({ currentUser, rules: sortedRules }); + } } - } - return []; -}; + return []; + }; diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index 87186c66167..bda297f6467 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -1,4 +1,7 @@ +import API from '@aws-amplify/api'; import { Amplify, ConsoleLogger as Logger, Hub, JS } from '@aws-amplify/core'; +import { Auth } from '@aws-amplify/auth'; +import Cache from '@aws-amplify/cache'; import { Draft, immerable, @@ -43,7 +46,6 @@ import { SchemaModel, SchemaNamespace, SchemaNonModel, - InternalSubscriptionMessage, SubscriptionMessage, DataStoreSnapshot, SyncConflict, @@ -55,6 +57,7 @@ import { isNonModelFieldType, isModelFieldType, ObserveQueryOptions, + AmplifyContext, } from '../types'; import { DATASTORE, @@ -70,6 +73,8 @@ import { registerNonModelClass, sortCompareFunction, DeferredCallbackResolver, + validatePredicate, + mergePatches, } from '../util'; setAutoFreeze(true); @@ -223,6 +228,20 @@ const initSchema = (userSchema: Schema) => { return userClasses; }; +/* Checks if the schema has been initialized by initSchema(). + * + * Call this function before accessing schema. + * Currently this only needs to be called in start() and clear() because all other functions will call start first. + */ +const checkSchemaInitialized = () => { + if (schema === undefined) { + const message = + 'Schema is not initialized. DataStore will not function as expected. This could happen if you have multiple versions of DataStore installed. Please see https://docs.amplify.aws/lib/troubleshooting/upgrading/q/platform/js/#check-for-duplicate-versions'; + logger.error(message); + throw new Error(message); + } +}; + const createTypeClasses: ( namespace: SchemaNamespace ) => TypeConstructorMap = namespace => { @@ -469,9 +488,21 @@ const createModelClass = ( p => (patches = p) ); - if (patches.length) { - modelPatchesMap.set(model, [patches, source]); - checkReadOnlyPropertyOnUpdate(patches, modelDefinition); + const hasExistingPatches = modelPatchesMap.has(source); + if (patches.length || hasExistingPatches) { + if (hasExistingPatches) { + const [existingPatches, existingSource] = modelPatchesMap.get(source); + const mergedPatches = mergePatches( + existingSource, + existingPatches, + patches + ); + modelPatchesMap.set(model, [mergedPatches, existingSource]); + checkReadOnlyPropertyOnUpdate(mergedPatches, modelDefinition); + } else { + modelPatchesMap.set(model, [patches, source]); + checkReadOnlyPropertyOnUpdate(patches, modelDefinition); + } } return model; @@ -565,7 +596,7 @@ function defaultConflictHandler(conflictData: SyncConflict): PersistentModel { return modelInstanceCreator(modelConstructor, { ...localModel, _version }); } -function defaultErrorHandler(error: SyncError) { +function defaultErrorHandler(error: SyncError): void { logger.warn(error); } @@ -683,10 +714,15 @@ function getNamespace(): SchemaNamespace { } class DataStore { + // reference to configured category instances. Used for preserving SSR context + private Auth = Auth; + private API = API; + private Cache = Cache; + private amplifyConfig: Record = {}; private authModeStrategy: AuthModeStrategy; private conflictHandler: ConflictHandler; - private errorHandler: (error: SyncError) => void; + private errorHandler: (error: SyncError) => void; private fullSyncInterval: number; private initialized: Promise; private initReject: Function; @@ -700,6 +736,12 @@ class DataStore { new WeakMap>(); private sessionId: string; private storageAdapter: Adapter; + // object that gets passed to descendent classes. Allows us to pass these down by reference + private amplifyContext: AmplifyContext = { + Auth: this.Auth, + API: this.API, + Cache: this.Cache, + }; getModuleName() { return 'DataStore'; @@ -729,6 +771,7 @@ class DataStore { await this.storage.init(); + checkSchemaInitialized(); await checkSchemaVersion(this.storage, schema.version); const { aws_appsync_graphqlEndpoint } = this.amplifyConfig; @@ -749,7 +792,8 @@ class DataStore { this.errorHandler, this.syncPredicates, this.amplifyConfig, - this.authModeStrategy + this.authModeStrategy, + this.amplifyContext ); // tslint:disable-next-line:max-line-length @@ -1129,24 +1173,32 @@ class DataStore { handle = this.storage .observe(modelConstructor, predicate) .filter(({ model }) => namespaceResolver(model) === USER) - .map( - (event: InternalSubscriptionMessage): SubscriptionMessage => { - // The `element` returned by storage only contains updated fields. - // Intercept the event to send the `savedElement` so that the first - // snapshot returned to the consumer contains all fields. - // In the event of a delete we return `element`, as `savedElement` - // here is undefined. - const { opType, model, condition, element, savedElement } = event; - - return { - opType, - element: savedElement || element, - model, - condition, - }; - } - ) - .subscribe(observer); + .subscribe({ + next: async item => { + // the `element` doesn't necessarily contain all item details or + // have related records attached consistently with that of a query() + // result item. for consistency, we attach them here. + + let message = item; + + // as lnog as we're not dealing with a DELETE, we need to fetch a fresh + // item from storage to ensure it's fully populated. + if (item.opType !== 'DELETE') { + const freshElement = await this.query( + item.model, + item.element.id + ); + message = { + ...message, + element: freshElement as T, + }; + } + + observer.next(message as SubscriptionMessage); + }, + error: err => observer.error(err), + complete: () => observer.complete(), + }); })(); return () => { @@ -1173,7 +1225,18 @@ class DataStore { const itemsChanged = new Map(); let deletedItemIds: string[] = []; let handle: ZenObservable.Subscription; - + let predicate: ModelPredicate; + + /** + * As the name suggests, this geneates a snapshot in the form of + * `{items: T[], isSynced: boolean}` + * and sends it to the observer. + * + * SIDE EFFECT: The underlying generation and emission methods may touch: + * `items`, `itemsChanged`, and `deletedItemIds`. + * + * Refer to `generateSnapshot` and `emitSnapshot` for more details. + */ const generateAndEmitSnapshot = (): void => { const snapshot = generateSnapshot(); emitSnapshot(snapshot); @@ -1190,6 +1253,28 @@ class DataStore { const { sort } = options || {}; const sortOptions = sort ? { sort } : undefined; + const modelDefinition = getModelDefinition(model); + if (isQueryOne(criteria)) { + predicate = ModelPredicateCreator.createForId( + modelDefinition, + criteria + ); + } else { + if (isPredicatesAll(criteria)) { + // Predicates.ALL means "all records", so no predicate (undefined) + predicate = undefined; + } else { + predicate = ModelPredicateCreator.createFromExisting( + modelDefinition, + criteria + ); + } + } + + const { predicates, type: predicateGroupType } = + ModelPredicateCreator.getPredicates(predicate, false) || {}; + const hasPredicate = !!predicates; + (async () => { try { // first, query and return any locally-available records @@ -1197,34 +1282,54 @@ class DataStore { items.set(item.id, item) ); - // observe the model and send a stream of updates (debounced) - handle = this.observe( - model, - // @ts-ignore TODO: fix this TSlint error - criteria - ).subscribe(({ element, model, opType }) => { - // Flag items which have been recently deleted - // NOTE: Merging of separate operations to the same model instance is handled upstream - // in the `mergePage` method within src/sync/merger.ts. The final state of a model instance - // depends on the LATEST record (for a given id). - if (opType === 'DELETE') { - deletedItemIds.push(element.id); - } else { - itemsChanged.set(element.id, element); - } + // Observe the model and send a stream of updates (debounced). + // We need to post-filter results instead of passing criteria through + // to have visibility into items that move from in-set to out-of-set. + // We need to explicitly remove those items from the existing snapshot. + handle = this.observe(model).subscribe( + ({ element, model, opType }) => { + if ( + hasPredicate && + !validatePredicate(element, predicateGroupType, predicates) + ) { + if ( + opType === 'UPDATE' && + (items.has(element.id) || itemsChanged.has(element.id)) + ) { + // tracking as a "deleted item" will include the item in + // page limit calculations and ensure it is removed from the + // final items collection, regardless of which collection(s) + // it is currently in. (I mean, it could be in both, right!?) + deletedItemIds.push(element.id); + } else { + // ignore updates for irrelevant/filtered items. + return; + } + } - const isSynced = this.sync?.getModelSyncedStatus(model) ?? false; + // Flag items which have been recently deleted + // NOTE: Merging of separate operations to the same model instance is handled upstream + // in the `mergePage` method within src/sync/merger.ts. The final state of a model instance + // depends on the LATEST record (for a given id). + if (opType === 'DELETE') { + deletedItemIds.push(element.id); + } else { + itemsChanged.set(element.id, element); + } - const limit = - itemsChanged.size - deletedItemIds.length >= this.syncPageSize; + const isSynced = this.sync?.getModelSyncedStatus(model) ?? false; - if (limit || isSynced) { - limitTimerRace.resolve(); - } + const limit = + itemsChanged.size - deletedItemIds.length >= this.syncPageSize; - // kicks off every subsequent race as results sync down - limitTimerRace.start(); - }); + if (limit || isSynced) { + limitTimerRace.resolve(); + } + + // kicks off every subsequent race as results sync down + limitTimerRace.start(); + } + ); // returns a set of initial/locally-available results generateAndEmitSnapshot(); @@ -1233,7 +1338,12 @@ class DataStore { } })(); - // TODO: abstract this function into a util file to be able to write better unit tests + /** + * Combines the `items`, `itemsChanged`, and `deletedItemIds` collections into + * a snapshot in the form of `{ items: T[], isSynced: boolean}`. + * + * SIDE EFFECT: The shared `items` collection is recreated. + */ const generateSnapshot = (): DataStoreSnapshot => { const isSynced = this.sync?.getModelSyncedStatus(model) ?? false; const itemsArray = [ @@ -1257,6 +1367,14 @@ class DataStore { }; }; + /** + * Emits the list of items to the observer. + * + * SIDE EFFECT: `itemsChanged` and `deletedItemIds` are cleared to prepare + * for the next snapshot. + * + * @param snapshot The generated items data to emit. + */ const emitSnapshot = (snapshot: DataStoreSnapshot): void => { // send the generated snapshot to the primary subscription observer.next(snapshot); @@ -1266,6 +1384,12 @@ class DataStore { deletedItemIds = []; }; + /** + * Sorts an `Array` of `T` according to the sort instructions given in the + * original `observeQuery()` call. + * + * @param itemsToSort A array of model type. + */ const sortItems = (itemsToSort: T[]): void => { const modelDefinition = getModelDefinition(model); const pagination = this.processPagination(modelDefinition, options); @@ -1280,7 +1404,14 @@ class DataStore { } }; - // send one last snapshot when the model is fully synced + /** + * Force one last snapshot when the model is fully synced. + * + * This reduces latency for that last snapshot, which will otherwise + * wait for the configured timeout. + * + * @param payload The payload from the Hub event. + */ const hubCallback = ({ payload }): void => { const { event, data } = payload; if ( @@ -1302,6 +1433,10 @@ class DataStore { }; configure = (config: DataStoreConfig = {}) => { + this.amplifyContext.Auth = this.Auth; + this.amplifyContext.API = this.API; + this.amplifyContext.Cache = this.Cache; + const { DataStore: configDataStore, authModeStrategyType: configAuthModeStrategyType, @@ -1331,7 +1466,7 @@ class DataStore { switch (authModeStrategyType) { case AuthModeStrategyType.MULTI_AUTH: - this.authModeStrategy = multiAuthStrategy; + this.authModeStrategy = multiAuthStrategy(this.amplifyContext); break; case AuthModeStrategyType.DEFAULT: this.authModeStrategy = defaultAuthStrategy; @@ -1384,6 +1519,7 @@ class DataStore { }; clear = async function clear() { + checkSchemaInitialized(); if (this.storage === undefined) { // connect to storage so that it can be cleared without fully starting DataStore this.storage = new Storage( diff --git a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts index 2729379533a..b38724d03fc 100644 --- a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts +++ b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts @@ -399,6 +399,7 @@ export class AsyncStorageAdapter implements Adapter { const nameSpace = this.namespaceResolver(modelConstructor); const storeName = this.getStorenameForModel(modelConstructor); + if (condition) { const fromDB = await this.db.get(model.id, storeName); diff --git a/packages/datastore/src/storage/adapter/InMemoryStore.ts b/packages/datastore/src/storage/adapter/InMemoryStore.ts index 9a771ba0186..865c8a3395d 100644 --- a/packages/datastore/src/storage/adapter/InMemoryStore.ts +++ b/packages/datastore/src/storage/adapter/InMemoryStore.ts @@ -12,7 +12,7 @@ export class InMemoryStore { multiRemove = async (keys: string[], callback?) => { keys.forEach(k => this.db.delete(k)); - callback(); + typeof callback === 'function' && callback(); }; multiSet = async (entries: string[][], callback?) => { @@ -20,7 +20,7 @@ export class InMemoryStore { this.setItem(key, value); }); - callback(); + typeof callback === 'function' && callback(); }; setItem = async (key: string, value: string) => { diff --git a/packages/datastore/src/sync/index.ts b/packages/datastore/src/sync/index.ts index 22c4eee6840..4d985b8a479 100644 --- a/packages/datastore/src/sync/index.ts +++ b/packages/datastore/src/sync/index.ts @@ -21,6 +21,7 @@ import { TypeConstructorMap, ModelPredicate, AuthModeStrategy, + AmplifyContext, } from '../types'; import { exhaustiveCheck, getNow, SYNC, USER } from '../util'; import DataStoreConnectivity from './datastoreConnectivity'; @@ -117,7 +118,8 @@ export class SyncEngine { errorHandler: ErrorHandler, private readonly syncPredicates: WeakMap>, private readonly amplifyConfig: Record = {}, - private readonly authModeStrategy: AuthModeStrategy + private readonly authModeStrategy: AuthModeStrategy, + private readonly amplifyContext: AmplifyContext ) { const MutationEvent = this.modelClasses[ 'MutationEvent' @@ -136,14 +138,20 @@ export class SyncEngine { this.schema, this.syncPredicates, this.amplifyConfig, - this.authModeStrategy + this.authModeStrategy, + errorHandler, + this.amplifyContext ); + this.subscriptionsProcessor = new SubscriptionProcessor( this.schema, this.syncPredicates, this.amplifyConfig, - this.authModeStrategy + this.authModeStrategy, + errorHandler, + this.amplifyContext ); + this.mutationsProcessor = new MutationProcessor( this.schema, this.storage, @@ -153,9 +161,11 @@ export class SyncEngine { MutationEvent, this.amplifyConfig, this.authModeStrategy, + errorHandler, conflictHandler, - errorHandler + this.amplifyContext ); + this.datastoreConnectivity = new DataStoreConnectivity(); } diff --git a/packages/datastore/src/sync/processors/errorMaps.ts b/packages/datastore/src/sync/processors/errorMaps.ts new file mode 100644 index 00000000000..85ab7fabfaa --- /dev/null +++ b/packages/datastore/src/sync/processors/errorMaps.ts @@ -0,0 +1,93 @@ +import { ErrorType } from '../../types'; + +export type ErrorMap = Partial<{ + [key in ErrorType]: (error: Error) => boolean; +}>; + +const connectionTimeout = error => + /^Connection failed: Connection Timeout/.test(error.message); + +const serverError = error => + /^Error: Request failed with status code 5\d\d/.test(error.message); + +export const mutationErrorMap: ErrorMap = { + BadModel: () => false, + BadRecord: error => { + const { message } = error; + return ( + /^Cannot return \w+ for [\w-_]+ type/.test(message) || + /^Variable '.+' has coerced Null value for NonNull type/.test(message) + ); // newly required field, out of date client + }, + ConfigError: () => false, + Transient: error => connectionTimeout(error) || serverError(error), + Unauthorized: error => + /^Request failed with status code 401/.test(error.message), +}; + +export const subscriptionErrorMap: ErrorMap = { + BadModel: () => false, + BadRecord: () => false, + ConfigError: () => false, + Transient: observableError => { + const error = unwrapObservableError(observableError); + return connectionTimeout(error) || serverError(error); + }, + Unauthorized: observableError => { + const error = unwrapObservableError(observableError); + return /Connection failed.+Unauthorized/.test(error.message); + }, +}; + +export const syncErrorMap: ErrorMap = { + BadModel: () => false, + BadRecord: error => /^Cannot return \w+ for [\w-_]+ type/.test(error.message), + ConfigError: () => false, + Transient: error => connectionTimeout(error) || serverError(error), + Unauthorized: () => false, +}; + +/** + * Get the first error reason of an observable. + * Allows for error maps to be easily applied to observable errors + * + * @param observableError an error from ZenObservable subscribe error callback + */ +function unwrapObservableError(observableError: any) { + const { + error: { errors: [error] } = { + errors: [], + }, + } = observableError; + + return error; +} + +export function getMutationErrorType(error: Error): ErrorType { + return mapErrorToType(mutationErrorMap, error); +} + +export function getSubscriptionErrorType(error: Error): ErrorType { + return mapErrorToType(subscriptionErrorMap, error); +} + +export function getSyncErrorType(error: Error): ErrorType { + return mapErrorToType(syncErrorMap, error); +} + +/** + * Categorizes an error with a broad error type, intended to make + * customer error handling code simpler. + * @param errorMap Error names and a list of patterns that indicate them (each pattern as a regex or function) + * @param error The underying error to categorize. + */ +export function mapErrorToType(errorMap: ErrorMap, error: Error): ErrorType { + const errorTypes = [...Object.keys(errorMap)] as ErrorType[]; + for (const errorType of errorTypes) { + const matcher = errorMap[errorType]; + if (matcher(error)) { + return errorType; + } + } + return 'Unknown'; +} diff --git a/packages/datastore/src/sync/processors/mutation.ts b/packages/datastore/src/sync/processors/mutation.ts index 036721941e5..b07d93d2cfe 100644 --- a/packages/datastore/src/sync/processors/mutation.ts +++ b/packages/datastore/src/sync/processors/mutation.ts @@ -24,8 +24,10 @@ import { PersistentModelConstructor, SchemaModel, TypeConstructorMap, + ProcessName, + AmplifyContext, } from '../../types'; -import { exhaustiveCheck, USER } from '../../util'; +import { exhaustiveCheck, USER, USER_AGENT_SUFFIX_DATASTORE } from '../../util'; import { MutationEventOutbox } from '../outbox'; import { buildGraphQLOperation, @@ -34,6 +36,7 @@ import { TransformerMutationType, getTokenForCustomAuth, } from '../utils'; +import { getMutationErrorType } from './errorMaps'; const MAX_ATTEMPTS = 10; @@ -63,9 +66,11 @@ class MutationProcessor { private readonly MutationEvent: PersistentModelConstructor, private readonly amplifyConfig: Record = {}, private readonly authModeStrategy: AuthModeStrategy, - private readonly conflictHandler?: ConflictHandler, - private readonly errorHandler?: ErrorHandler + private readonly errorHandler: ErrorHandler, + private readonly conflictHandler: ConflictHandler, + private readonly amplifyContext: AmplifyContext ) { + this.amplifyContext.API = this.amplifyContext.API || API; this.generateQueries(); } @@ -266,7 +271,13 @@ class MutationProcessor { this.amplifyConfig ); - const tryWith = { query, variables, authMode, authToken }; + const tryWith = { + query, + variables, + authMode, + authToken, + userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, + }; let attempt = 0; const opType = this.opTypeFromTransformerOperation(operation); @@ -274,7 +285,7 @@ class MutationProcessor { do { try { const result = >>( - await API.graphql(tryWith) + await this.amplifyContext.API.graphql(tryWith) ); return [result, opName, modelDefinition]; } catch (err) { @@ -341,11 +352,12 @@ class MutationProcessor { const serverData = < GraphQLResult> - >await API.graphql({ + >await this.amplifyContext.API.graphql({ query, variables: { id: variables.input.id }, authMode, authToken, + userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, }); return [serverData, opName, modelDefinition]; @@ -373,20 +385,21 @@ class MutationProcessor { } else { try { await this.errorHandler({ - localModel: this.modelInstanceCreator( - modelConstructor, - variables.input - ), + recoverySuggestion: + 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues', + localModel: variables.input, message: error.message, operation, - errorType: error.errorType, + errorType: getMutationErrorType(error), errorInfo: error.errorInfo, + process: ProcessName.mutate, + cause: error, remoteModel: error.data ? this.modelInstanceCreator(modelConstructor, error.data) : null, }); } catch (err) { - logger.warn('failed to execute errorHandler', err); + logger.warn('Mutation error handler failed with:', err); } finally { // Return empty tuple, dequeues the mutation return error.data diff --git a/packages/datastore/src/sync/processors/subscription.ts b/packages/datastore/src/sync/processors/subscription.ts index 7132c9072ca..ee1f9cea4b7 100644 --- a/packages/datastore/src/sync/processors/subscription.ts +++ b/packages/datastore/src/sync/processors/subscription.ts @@ -1,5 +1,5 @@ import API, { GraphQLResult, GRAPHQL_AUTH_MODE } from '@aws-amplify/api'; -import Auth from '@aws-amplify/auth'; +import { Auth } from '@aws-amplify/auth'; import Cache from '@aws-amplify/cache'; import { ConsoleLogger as Logger, Hub, HubCapsule } from '@aws-amplify/core'; import { CONTROL_MSG as PUBSUB_CONTROL_MSG } from '@aws-amplify/pubsub'; @@ -12,6 +12,9 @@ import { PredicatesGroup, ModelPredicate, AuthModeStrategy, + ErrorHandler, + ProcessName, + AmplifyContext, } from '../../types'; import { buildSubscriptionGraphQLOperation, @@ -22,7 +25,8 @@ import { getTokenForCustomAuth, } from '../utils'; import { ModelPredicateCreator } from '../../predicates'; -import { validatePredicate } from '../../util'; +import { validatePredicate, USER_AGENT_SUFFIX_DATASTORE } from '../../util'; +import { getSubscriptionErrorType } from './errorMaps'; const logger = new Logger('DataStore'); @@ -56,7 +60,9 @@ class SubscriptionProcessor { private readonly schema: InternalSchema, private readonly syncPredicates: WeakMap>, private readonly amplifyConfig: Record = {}, - private readonly authModeStrategy: AuthModeStrategy + private readonly authModeStrategy: AuthModeStrategy, + private readonly errorHandler: ErrorHandler, + private readonly amplifyContext: AmplifyContext = { Auth, API, Cache } ) {} private buildSubscription( @@ -249,8 +255,8 @@ class SubscriptionProcessor { (async () => { try { // retrieving current AWS Credentials - // TODO Should this use `this.amplify.Auth` for SSR? - const credentials = await Auth.currentCredentials(); + const credentials = + await this.amplifyContext.Auth.currentCredentials(); userCredentials = credentials.authenticated ? USER_CREDENTIALS.auth : USER_CREDENTIALS.unauth; @@ -260,8 +266,7 @@ class SubscriptionProcessor { try { // retrieving current token info from Cognito UserPools - // TODO Should this use `this.amplify.Auth` for SSR? - const session = await Auth.currentSession(); + const session = await this.amplifyContext.Auth.currentSession(); cognitoTokenPayload = session.getIdToken().decodePayload(); } catch (err) { // best effort to get jwt from Cognito @@ -278,11 +283,14 @@ class SubscriptionProcessor { let token; // backwards compatibility - const federatedInfo = await Cache.getItem('federatedInfo'); + const federatedInfo = await this.amplifyContext.Cache.getItem( + 'federatedInfo' + ); if (federatedInfo) { token = federatedInfo.token; } else { - const currentUser = await Auth.currentAuthenticatedUser(); + const currentUser = + await this.amplifyContext.Auth.currentAuthenticatedUser(); if (currentUser) { token = currentUser.token; } @@ -379,18 +387,24 @@ class SubscriptionProcessor { }` ); + const userAgentSuffix = USER_AGENT_SUFFIX_DATASTORE; + const queryObservable = < Observable<{ value: GraphQLResult>; }> - >(API.graphql({ query, variables, ...{ authMode }, authToken })); + + >(this.amplifyContext.API.graphql({ query, variables, ...{ authMode }, authToken, userAgentSuffix })); + let subscriptionReadyCallback: () => void; subscriptions[modelDefinition.name][ transformerMutationType ].push( queryObservable - .map(({ value }) => value) + .map(({ value }) => { + return value; + }) .subscribe({ next: ({ data, errors }) => { if (Array.isArray(errors) && errors.length > 0) { @@ -436,7 +450,7 @@ class SubscriptionProcessor { } this.drainBuffer(); }, - error: subscriptionError => { + error: async subscriptionError => { const { error: { errors: [{ message = '' } = {}] } = { errors: [], @@ -462,6 +476,7 @@ class SubscriptionProcessor { operationAuthModeAttempts[operation] >= readAuthModes.length ) { + // last auth mode retry. Continue with error logger.debug( `${operation} subscription failed with authMode: ${ readAuthModes[ @@ -469,9 +484,9 @@ class SubscriptionProcessor { ] }` ); - logger.warn('subscriptionError', message); - return; } else { + // retry with different auth mode. Do not trigger + // observer error or error handler logger.debug( `${operation} subscription failed with authMode: ${ readAuthModes[ @@ -487,9 +502,29 @@ class SubscriptionProcessor { return; } } - logger.warn('subscriptionError', message); + try { + await this.errorHandler({ + recoverySuggestion: + 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues', + localModel: null, + message, + model: modelDefinition.name, + operation, + errorType: + getSubscriptionErrorType(subscriptionError), + process: ProcessName.subscribe, + remoteModel: null, + cause: subscriptionError, + }); + } catch (e) { + logger.error( + 'Subscription error handler failed with:', + e + ); + } + if (typeof subscriptionReadyCallback === 'function') { subscriptionReadyCallback(); } @@ -500,7 +535,6 @@ class SubscriptionProcessor { ) { return; } - observer.error(message); }, }) diff --git a/packages/datastore/src/sync/processors/sync.ts b/packages/datastore/src/sync/processors/sync.ts index e0f07fd8b0c..eb6fc5226ff 100644 --- a/packages/datastore/src/sync/processors/sync.ts +++ b/packages/datastore/src/sync/processors/sync.ts @@ -8,6 +8,9 @@ import { PredicatesGroup, GraphQLFilter, AuthModeStrategy, + ErrorHandler, + ProcessName, + AmplifyContext, } from '../../types'; import { buildGraphQLOperation, @@ -17,6 +20,7 @@ import { predicateToGraphQLFilter, getTokenForCustomAuth, } from '../utils'; +import { USER_AGENT_SUFFIX_DATASTORE } from '../../util'; import { jitteredExponentialRetry, ConsoleLogger as Logger, @@ -24,7 +28,7 @@ import { NonRetryableError, } from '@aws-amplify/core'; import { ModelPredicateCreator } from '../../predicates'; - +import { getSyncErrorType } from './errorMaps'; const opResultDefaults = { items: [], nextToken: null, @@ -40,8 +44,11 @@ class SyncProcessor { private readonly schema: InternalSchema, private readonly syncPredicates: WeakMap>, private readonly amplifyConfig: Record = {}, - private readonly authModeStrategy: AuthModeStrategy + private readonly authModeStrategy: AuthModeStrategy, + private readonly errorHandler: ErrorHandler, + private readonly amplifyContext: AmplifyContext ) { + amplifyContext.API = amplifyContext.API || API; this.generateQueries(); } @@ -203,11 +210,12 @@ class SyncProcessor { this.amplifyConfig ); - return await API.graphql({ + return await this.amplifyContext.API.graphql({ query, variables, authMode, authToken, + userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, }); } catch (error) { // Catch client-side (GraphQLAuthError) & 401/403 errors here so that we don't continue to retry @@ -223,15 +231,33 @@ class SyncProcessor { error.data[opName] && error.data[opName].items ); - if (this.partialDataFeatureFlagEnabled()) { if (hasItems) { const result = error; result.data[opName].items = result.data[opName].items.filter( item => item !== null ); - if (error.errors) { + await Promise.all( + error.errors.map(async err => { + try { + await this.errorHandler({ + recoverySuggestion: + 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues', + localModel: null, + message: err.message, + model: modelDefinition.name, + operation: opName, + errorType: getSyncErrorType(err), + process: ProcessName.sync, + remoteModel: null, + cause: err, + }); + } catch (e) { + logger.error('Sync error handler failed with:', e); + } + }) + ); Hub.dispatch('datastore', { event: 'syncQueriesPartialSyncError', data: { diff --git a/packages/datastore/src/types.ts b/packages/datastore/src/types.ts index 32d1869f1fe..636a0d078d7 100644 --- a/packages/datastore/src/types.ts +++ b/packages/datastore/src/types.ts @@ -13,6 +13,9 @@ import { } from './util'; import { PredicateAll } from './predicates'; import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api-graphql'; +import { Auth } from '@aws-amplify/auth'; +import { API } from '@aws-amplify/api'; +import Cache from '@aws-amplify/cache'; import { Adapter } from './storage/adapter'; //#region Schema types @@ -658,7 +661,7 @@ export type DataStoreConfig = { DataStore?: { authModeStrategyType?: AuthModeStrategyType; conflictHandler?: ConflictHandler; // default : retry until client wins up to x times - errorHandler?: (error: SyncError) => void; // default : logger.warn + errorHandler?: (error: SyncError) => void; // default : logger.warn maxRecordsToSync?: number; // merge syncPageSize?: number; fullSyncInterval?: number; @@ -668,7 +671,7 @@ export type DataStoreConfig = { }; authModeStrategyType?: AuthModeStrategyType; conflictHandler?: ConflictHandler; // default : retry until client wins up to x times - errorHandler?: (error: SyncError) => void; // default : logger.warn + errorHandler?: (error: SyncError) => void; // default : logger.warn maxRecordsToSync?: number; // merge syncPageSize?: number; fullSyncInterval?: number; @@ -775,15 +778,33 @@ export type SyncConflict = { attempts: number; }; -export type SyncError = { +export type SyncError = { message: string; - errorType: string; - errorInfo: string; - localModel: PersistentModel; - remoteModel: PersistentModel; + errorType: ErrorType; + errorInfo?: string; + recoverySuggestion?: string; + model?: string; + localModel: T; + remoteModel: T; + process: ProcessName; operation: string; + cause?: Error; }; +export type ErrorType = + | 'ConfigError' + | 'BadModel' + | 'BadRecord' + | 'Unauthorized' + | 'Transient' + | 'Unknown'; + +export enum ProcessName { + 'sync' = 'sync', + 'mutate' = 'mutate', + 'subscribe' = 'subscribe', +} + export const DISCARD = Symbol('DISCARD'); export type ConflictHandler = ( @@ -792,7 +813,7 @@ export type ConflictHandler = ( | Promise | PersistentModel | typeof DISCARD; -export type ErrorHandler = (error: SyncError) => void; +export type ErrorHandler = (error: SyncError) => void; export type DeferredCallbackResolverOptions = { callback: () => void; @@ -805,3 +826,9 @@ export enum LimitTimerRaceResolvedValues { TIMER = 'TIMER', } //#endregion + +export type AmplifyContext = { + Auth: typeof Auth; + API: typeof API; + Cache: typeof Cache; +}; diff --git a/packages/datastore/src/util.ts b/packages/datastore/src/util.ts index 0e5364281ce..08b5c74bc3e 100644 --- a/packages/datastore/src/util.ts +++ b/packages/datastore/src/util.ts @@ -1,6 +1,7 @@ import { Buffer } from 'buffer'; import { monotonicFactory, ULID } from 'ulid'; import { v4 as uuid } from 'uuid'; +import { produce, applyPatches, Patch } from 'immer'; import { ModelInstanceCreator } from './datastore/datastore'; import { AllOperators, @@ -28,6 +29,21 @@ import { } from './types'; import { WordArray } from 'amazon-cognito-identity-js'; +export enum NAMESPACES { + DATASTORE = 'datastore', + USER = 'user', + SYNC = 'sync', + STORAGE = 'storage', +} + +const DATASTORE = NAMESPACES.DATASTORE; +const USER = NAMESPACES.USER; +const SYNC = NAMESPACES.SYNC; +const STORAGE = NAMESPACES.STORAGE; + +export { USER, SYNC, STORAGE, DATASTORE }; +export const USER_AGENT_SUFFIX_DATASTORE = '/DataStore'; + export const exhaustiveCheck = (obj: never, throwOnError: boolean = true) => { if (throwOnError) { throw new Error(`Invalid ${obj}`); @@ -146,7 +162,7 @@ export const isNonModelConstructor = ( return nonModelClasses.has(obj); }; -/* +/* When we have GSI(s) with composite sort keys defined on a model There are some very particular rules regarding which fields must be included in the update mutation input The field selection becomes more complex as the number of GSIs with composite sort keys grows @@ -156,7 +172,7 @@ export const isNonModelConstructor = ( 2. all of the fields from any other composite sort key that intersect with the fields from 1. E.g., - Model @model + Model @model @key(name: 'key1' fields: ['hk', 'a', 'b', 'c']) @key(name: 'key2' fields: ['hk', 'a', 'b', 'd']) @key(name: 'key3' fields: ['hk', 'x', 'y', 'z']) @@ -192,7 +208,7 @@ export const processCompositeKeys = ( .filter(isModelAttributeCompositeKey) .map(extractCompositeSortKey); - /* + /* if 2 sets of fields have any intersecting fields => combine them into 1 union set e.g., ['a', 'b', 'c'] and ['a', 'b', 'd'] => ['a', 'b', 'c', 'd'] */ @@ -448,20 +464,6 @@ export const getIndexFromAssociation = ( return index; }; -export enum NAMESPACES { - DATASTORE = 'datastore', - USER = 'user', - SYNC = 'sync', - STORAGE = 'storage', -} - -const DATASTORE = NAMESPACES.DATASTORE; -const USER = NAMESPACES.USER; -const SYNC = NAMESPACES.SYNC; -const STORAGE = NAMESPACES.STORAGE; - -export { USER, SYNC, STORAGE, DATASTORE }; - let privateModeCheckResult; export const isPrivateMode = () => { @@ -773,3 +775,39 @@ export class DeferredCallbackResolver { this.limitPromise.resolve(LimitTimerRaceResolvedValues.LIMIT); } } + +/** + * merge two sets of patches created by immer produce. + * newPatches take precedent over oldPatches for patches modifying the same path. + * In the case many consecutive pathces are merged the original model should + * always be the root model. + * + * Example: + * A -> B, patches1 + * B -> C, patches2 + * + * mergePatches(A, patches1, patches2) to get patches for A -> C + * + * @param originalSource the original Model the patches should be applied to + * @param oldPatches immer produce patch list + * @param newPatches immer produce patch list (will take precedence) + * @return merged patches + */ +export function mergePatches( + originalSource: T, + oldPatches: Patch[], + newPatches: Patch[] +): Patch[] { + const patchesToMerge = oldPatches.concat(newPatches); + let patches: Patch[]; + produce( + originalSource, + draft => { + applyPatches(draft, patchesToMerge); + }, + p => { + patches = p; + } + ); + return patches; +} diff --git a/packages/geo/CHANGELOG.md b/packages/geo/CHANGELOG.md index b665a0fc45a..15d2aa75d7d 100644 --- a/packages/geo/CHANGELOG.md +++ b/packages/geo/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.3.14](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.13...@aws-amplify/geo@1.3.14) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.13](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.12...@aws-amplify/geo@1.3.13) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.12](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.11...@aws-amplify/geo@1.3.12) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.11](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.10...@aws-amplify/geo@1.3.11) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.9...@aws-amplify/geo@1.3.10) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.8...@aws-amplify/geo@1.3.9) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.7...@aws-amplify/geo@1.3.8) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.6...@aws-amplify/geo@1.3.7) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.5...@aws-amplify/geo@1.3.6) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.4...@aws-amplify/geo@1.3.5) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.3...@aws-amplify/geo@1.3.4) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.2...@aws-amplify/geo@1.3.3) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + +## [1.3.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.1...@aws-amplify/geo@1.3.2) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/geo + + + + + ## [1.3.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/geo@1.3.0...@aws-amplify/geo@1.3.1) (2022-04-14) **Note:** Version bump only for package @aws-amplify/geo diff --git a/packages/geo/__tests__/util.test.ts b/packages/geo/__tests__/util.test.ts index d811608e5c6..ef6373b6fc6 100644 --- a/packages/geo/__tests__/util.test.ts +++ b/packages/geo/__tests__/util.test.ts @@ -15,6 +15,7 @@ import { validateCoordinates, validateLinearRing, validatePolygon, + validateGeofenceId, validateGeofencesInput, } from '../src/util'; @@ -127,6 +128,40 @@ describe('Geo utility functions', () => { }); }); + describe('validateGeofenceId', () => { + test('should not throw an error for a geofence ID with letters and numbers', () => { + expect(() => validateGeofenceId('ExampleGeofence1')).not.toThrowError(); + }); + + test('should not throw an error for a geofence ID with a dash', () => { + expect(() => validateGeofenceId('ExampleGeofence-1')).not.toThrowError(); + }); + + test('should not throw an error for a geofence ID with a period', () => { + expect(() => validateGeofenceId('ExampleGeofence.1')).not.toThrowError(); + }); + + test('should not throw an error for a geofence ID with an underscore', () => { + expect(() => validateGeofenceId('ExampleGeofence_1')).not.toThrowError(); + }); + + test('should not throw an error for a geofence ID with non-basic Latin character', () => { + expect(() => validateGeofenceId('ExampleGeòfence-1')).not.toThrowError(); + }); + + test('should not throw an error for a geofence ID with superscript and subscript numbers', () => { + expect(() => validateGeofenceId('ExampleGeofence-⁴₆')).not.toThrowError(); + }); + + test('should throw an error for an empty string', () => { + expect(() => validateGeofenceId('')).toThrowError(); + }); + + test('should throw an error for a geofence ID with an invalid character', () => { + expect(() => validateGeofenceId('ExampleGeofence-1&')).toThrowError(); + }); + }); + describe('validateGeofencesInput', () => { test('should not throw an error for valid geofences', () => { const result = validateGeofencesInput(validGeofences); diff --git a/packages/geo/package.json b/packages/geo/package.json index 9bae6d9c4d4..df63e3208ce 100644 --- a/packages/geo/package.json +++ b/packages/geo/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/geo", - "version": "1.3.1", + "version": "1.3.14", "description": "Geo category for aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -41,7 +41,7 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/core": "4.5.2", + "@aws-amplify/core": "4.7.2", "@aws-sdk/client-location": "3.48.0", "@turf/boolean-clockwise": "6.5.0", "camelcase-keys": "6.2.2" diff --git a/packages/interactions/CHANGELOG.md b/packages/interactions/CHANGELOG.md index b317d61ec0e..8af22fa40bc 100644 --- a/packages/interactions/CHANGELOG.md +++ b/packages/interactions/CHANGELOG.md @@ -3,6 +3,116 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.50...@aws-amplify/interactions@4.0.51) (2022-08-23) + + +### Bug Fixes + +* **interactions:** fix addPluggable API ([#10250](https://github.com/aws-amplify/amplify-js/issues/10250)) ([01aad60](https://github.com/aws-amplify/amplify-js/commit/01aad60ff14c3db47761db819dd47def75bfcb9d)) + + + + + +## [4.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.49...@aws-amplify/interactions@4.0.50) (2022-08-18) + + +### Bug Fixes + +* **interactions:** fix configure default provider ([#10215](https://github.com/aws-amplify/amplify-js/issues/10215)) ([d4c3955](https://github.com/aws-amplify/amplify-js/commit/d4c395520bc66f24325babbe53e6ab7ebdea4d3b)) + + + + + +## [4.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.48...@aws-amplify/interactions@4.0.49) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.47...@aws-amplify/interactions@4.0.48) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.46...@aws-amplify/interactions@4.0.47) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.45...@aws-amplify/interactions@4.0.46) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.44...@aws-amplify/interactions@4.0.45) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.43...@aws-amplify/interactions@4.0.44) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.42...@aws-amplify/interactions@4.0.43) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.41...@aws-amplify/interactions@4.0.42) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.40...@aws-amplify/interactions@4.0.41) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.39...@aws-amplify/interactions@4.0.40) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + +## [4.0.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.38...@aws-amplify/interactions@4.0.39) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/interactions + + + + + ## [4.0.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/interactions@4.0.37...@aws-amplify/interactions@4.0.38) (2022-04-14) **Note:** Version bump only for package @aws-amplify/interactions diff --git a/packages/interactions/__tests__/Interactions-unit-test.ts b/packages/interactions/__tests__/Interactions-unit-test.ts index 114b54b051f..87fe5c2aa25 100644 --- a/packages/interactions/__tests__/Interactions-unit-test.ts +++ b/packages/interactions/__tests__/Interactions-unit-test.ts @@ -1,113 +1,124 @@ +/* + * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ import { InteractionsClass as Interactions } from '../src/Interactions'; -import { AWSLexProvider, AbstractInteractionsProvider } from '../src/Providers'; -import { Credentials } from '@aws-amplify/core'; -import { - LexRuntimeServiceClient, - PostContentCommand, - PostTextCommand, -} from '@aws-sdk/client-lex-runtime-service'; +import { AbstractInteractionsProvider } from '../src/Providers'; +import { InteractionsOptions } from '../src/types'; +import { AWSLexProvider } from '../src/Providers'; (global as any).Response = () => {}; (global as any).Response.prototype.arrayBuffer = (blob: Blob) => { return Promise.resolve(new ArrayBuffer(0)); }; -// mock stream response -const createBlob = () => { - return new Blob(); +// aws-export config +const awsmobileBot = { + name: 'BookTripMOBILEHUB', + alias: '$LATEST', + region: 'us-east-1', + providerName: 'DummyProvider', + description: 'Bot to make reservations for a visit to a city.', + 'bot-template': 'bot-trips', + 'commands-help': [ + 'Book a car', + 'Reserve a car', + 'Make a car reservation', + 'Book a hotel', + 'Reserve a room', + 'I want to make a hotel reservation', + ], +}; +const awsmobile = { + aws_bots: 'enable', + aws_bots_config: [awsmobileBot], + aws_project_name: 'bots', + aws_project_region: 'us-east-1', }; -LexRuntimeServiceClient.prototype.send = jest.fn((command, callback) => { - if (command instanceof PostTextCommand) { - if (command.input.inputText === 'done') { - const result = { - message: 'echo:' + command.input.inputText, - dialogState: 'ReadyForFulfillment', - slots: { - m1: 'hi', - m2: 'done', - }, - }; - return Promise.resolve(result); - } else { - const result = { - message: 'echo:' + command.input.inputText, - dialogState: 'ElicitSlot', - }; - return Promise.resolve(result); - } - } else if (command instanceof PostContentCommand) { - if (command.input.contentType === 'audio/x-l16; sample-rate=16000') { - if (command.input.inputStream === 'voice:done') { - const result = { - message: 'voice:echo:' + command.input.inputStream, - dialogState: 'ReadyForFulfillment', - slots: { - m1: 'voice:hi', - m2: 'voice:done', - }, - audioStream: createBlob(), - }; - return Promise.resolve(result); - } else { - const result = { - message: 'voice:echo:' + command.input.inputStream, - dialogState: 'ElicitSlot', - audioStream: createBlob(), - }; - return Promise.resolve(result); - } - } else { - if (command.input.inputStream === 'done') { - const result = { - message: 'echo:' + command.input.inputStream, - dialogState: 'ReadyForFulfillment', - slots: { - m1: 'hi', - m2: 'done', - }, - audioStream: createBlob(), - }; - return Promise.resolve(result); - } else { - const result = { - message: 'echo:' + command.input.inputStream, - dialogState: 'ElicitSlot', - audioStream: createBlob(), - }; - return Promise.resolve(result); - } - } - } -}) as any; +// manual config +const manualConfigBots = { + BookTrip: { + name: 'BookTrip', + alias: '$LATEST', + region: 'us-west-2', + providerName: 'DummyProvider', + }, + OrderFlowers: { + name: 'OrderFlowers', + alias: '$LATEST', + region: 'us-west-2', + providerName: 'DummyProvider', + }, +}; +const manualConfig = { + Interactions: { + bots: manualConfigBots, + }, +}; + +// a sample response from send method +const sampleSendResponse = { + $metadata: { + httpStatusCode: 200, + requestId: '6eed4ad1-141c-4662-a528-3c857de1e1da', + attempts: 1, + totalRetryDelay: 0, + }, + alternativeIntents: '[]', + audioStream: new Blob(), + botVersion: '$LATEST', + contentType: 'audio/mpeg', + dialogState: 'ElicitSlot', + intentName: 'BookCar_dev', + message: 'In what city do you need to rent a car?', + sessionId: '2022-08-11T18:23:01.013Z-sTqDnpGk', + slotToElicit: 'PickUpCity', + slots: + '{"ReturnDate":null,"PickUpDate":null,"DriverAge":null,"CarType":null,"PickUpCity":null,"Location":null}', +}; -class AWSLexProvider2 extends AWSLexProvider { +class DummyProvider extends AbstractInteractionsProvider { getProviderName() { - return 'AWSLexProvider2'; + return 'DummyProvider'; + } + + configure(config: InteractionsOptions = {}): InteractionsOptions { + return super.configure(config); } -} -class AWSLexProviderWrong extends AbstractInteractionsProvider { - private onCompleteResolve: Function; - private onCompleteReject: Function; + async sendMessage(message: string | Object): Promise { + return new Promise(async (res, rej) => res(sampleSendResponse)); + } + async onComplete(botname: string, callback: (err, confirmation) => void) { + return new Promise((res, rej) => res({})); + } +} + +class WrongProvider extends AbstractInteractionsProvider { getProviderName() { - return 'AWSLexProviderWrong'; + return 'WrongProvider'; } getCategory() { - return 'IDontKnow'; + return 'WrongCategory'; } - sendMessage(message: string | Object): Promise { - return new Promise(async (res, rej) => {}); + async sendMessage(message: string | Object): Promise { + return new Promise(async (res, rej) => res({})); } async onComplete() { - return new Promise((res, rej) => { - this.onCompleteResolve = res; - this.onCompleteReject = rej; - }); + return new Promise((res, rej) => res({})); } } @@ -116,817 +127,309 @@ afterEach(() => { }); describe('Interactions', () => { - describe('constructor test', () => { - test('happy case', () => { - const interactions = new Interactions({}); + // Test 'configure' API + describe('configure API', () => { + let interactions; + let providerConfigureSpy; + + beforeEach(() => { + interactions = new Interactions({}); + interactions.configure({}); + interactions.addPluggable(new DummyProvider()); + providerConfigureSpy = jest.spyOn(DummyProvider.prototype, 'configure'); }); - }); - - describe('configure test', () => { - test('happy case', () => { - const interactions = new Interactions({}); + test('Check if bot is successfully configured by validating config response', () => { const options = { - key: 'value', + keyA: 'valueA', + keyB: 'valueB', }; const config = interactions.configure(options); - - expect(config).toEqual({ bots: {}, key: 'value' }); + expect(config).toEqual({ ...options, bots: {} }); + expect.assertions(1); }); - test('aws-exports configuration and send message to existing bot', async () => { - const curCredSpyOn = jest - .spyOn(Credentials, 'get') - .mockImplementationOnce(() => Promise.resolve({ identityId: '1234' })); - - const awsmobile = { - aws_bots: 'enable', - aws_bots_config: [ - { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - description: 'Bot to make reservations for a visit to a city.', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', - }; - const interactions = new Interactions({}); - + test('Configure bot using aws-exports configuration', () => { const config = interactions.configure(awsmobile); - expect(config).toEqual({ - aws_bots: 'enable', - aws_bots_config: [ - { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', + ...awsmobile, bots: { - BookTripMOBILEHUB: { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, + BookTripMOBILEHUB: awsmobileBot, }, }); - - const response = await interactions.send('BookTripMOBILEHUB', 'hi'); - - expect(response).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi', + // check if provider's configure was called + expect(providerConfigureSpy).toBeCalledTimes( + awsmobile.aws_bots_config.length + ); + expect(providerConfigureSpy).toHaveBeenCalledWith({ + BookTripMOBILEHUB: awsmobileBot, }); + expect.assertions(3); }); - test('aws-exports configuration with two bots and send message to existing bot', async () => { - const curCredSpyOn = jest - .spyOn(Credentials, 'get') - .mockImplementation(() => Promise.resolve({ identityId: '1234' })); - - const awsmobile = { - aws_bots: 'enable', - aws_bots_config: [ - { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - description: 'Bot to make reservations for a visit to a city.', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - region: 'us-east-1', - }, - { - name: 'BookTripMOBILEHUB2', - alias: '$LATEST', - description: 'Bot to make reservations for a visit to a city.', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', - }; - const interactions = new Interactions({}); - - const config = interactions.configure(awsmobile); - + test('Configure bot using manual configuration', () => { + const config = interactions.configure(manualConfig); expect(config).toEqual({ - aws_bots: 'enable', - aws_bots_config: [ - { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, - { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB2', - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', - bots: { - BookTripMOBILEHUB: { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, - BookTripMOBILEHUB2: { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB2', - region: 'us-east-1', - }, - }, + bots: manualConfigBots, }); - const response = await interactions.send('BookTripMOBILEHUB', 'hi'); + // check if provider's configure was called + expect(providerConfigureSpy).toBeCalledTimes( + Object.keys(manualConfigBots).length + ); - expect(response).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi', + // provider's config get's called for each bot + expect(providerConfigureSpy).toHaveBeenCalledWith({ + BookTrip: manualConfigBots.BookTrip, }); - - const response2 = await interactions.send('BookTripMOBILEHUB2', 'hi2'); - - expect(response2).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi2', + expect(providerConfigureSpy).toHaveBeenCalledWith({ + OrderFlowers: manualConfigBots.OrderFlowers, }); + expect.assertions(4); + }); - const interactionsMessageVoice = { - content: 'voice:hi', - options: { - messageType: 'voice', - }, + test('Configure bot using aws-exports and manual configuration', () => { + const combinedConfig = { + ...awsmobile, + ...manualConfig, }; - const interactionsMessageText = { - content: 'hi', - options: { - messageType: 'text', - }, - }; + const config = interactions.configure(combinedConfig); - const responseVoice = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageVoice - ); - expect(responseVoice).toEqual({ - dialogState: 'ElicitSlot', - message: 'voice:echo:voice:hi', - audioStream: new Uint8Array(), + // if manualConfig bots are given, aws-export bots are ignored + expect(config).toEqual({ + bots: manualConfigBots, }); - const responseText = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageText + // check if provider's configure was called + expect(providerConfigureSpy).toBeCalledTimes( + Object.keys(manualConfigBots).length ); - expect(responseText).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi', - audioStream: new Uint8Array(), + + // provider's config get's called for each bot + expect(providerConfigureSpy).toHaveBeenCalledWith({ + BookTrip: manualConfigBots.BookTrip, }); + expect(providerConfigureSpy).toHaveBeenCalledWith({ + OrderFlowers: manualConfigBots.OrderFlowers, + }); + expect.assertions(4); }); - test('Interactions configuration with two bots and send message to existing bot and fullfil', async () => { - const curCredSpyOn = jest - .spyOn(Credentials, 'get') - .mockImplementation(() => Promise.resolve({ identityId: '1234' })); - const configuration = { + test('Configure bot with default provider (AWSLexProvider) using manual config', async () => { + const lexV1ConfigureSpy = jest.spyOn( + AWSLexProvider.prototype, + 'configure' + ); + + const myBot = { + MyBot: { + name: 'MyBot', // default provider 'AWSLexProvider' + alias: '$LATEST', + region: 'us-west-2', + }, + }; + const myConfig = { Interactions: { - bots: { - BookTripMOBILEHUB: { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - region: 'us-east-1', - }, - BookTripMOBILEHUB2: { - name: 'BookTripMOBILEHUB2', - alias: '$LATEST', - region: 'us-east-1', - }, - }, + bots: myBot, }, }; - const interactions = new Interactions({}); + interactions.configure(myConfig); - const config = interactions.configure(configuration); - - expect(config).toEqual(configuration.Interactions); - const response = await interactions.send('BookTripMOBILEHUB', 'hi'); - - expect(response).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi', + // check if provider's configure was called + expect(lexV1ConfigureSpy).toBeCalledTimes(Object.keys(myBot).length); + expect(lexV1ConfigureSpy).toHaveBeenCalledWith({ + MyBot: myBot.MyBot, }); + expect.assertions(2); + }); - const response2 = await interactions.send('BookTripMOBILEHUB2', 'hi2'); - - expect(response2).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi2', - }); + test('Configure bot with default provider (AWSLexProvider) using aws-exports config', async () => { + const lexV1ConfigureSpy = jest.spyOn( + AWSLexProvider.prototype, + 'configure' + ); - const interactionsMessageVoice = { - content: 'voice:hi', - options: { - messageType: 'voice', - }, + const awsmobileBot = { + name: 'BookTripMOBILEHUB', + alias: '$LATEST', + region: 'us-east-1', + description: 'Bot to make reservations for a visit to a city.', + 'bot-template': 'bot-trips', }; - - const interactionsMessageText = { - content: 'hi', - options: { - messageType: 'text', - }, + const awsmobile = { + aws_bots: 'enable', + aws_bots_config: [awsmobileBot], + aws_project_name: 'bots', + aws_project_region: 'us-east-1', }; - const responseVoice = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageVoice - ); - expect(responseVoice).toEqual({ - dialogState: 'ElicitSlot', - message: 'voice:echo:voice:hi', - audioStream: new Uint8Array(), - }); + interactions.configure(awsmobile); - const responseText = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageText + // check if provider's configure was called + expect(lexV1ConfigureSpy).toBeCalledTimes( + awsmobile.aws_bots_config.length ); - expect(responseText).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi', - audioStream: new Uint8Array(), + expect(lexV1ConfigureSpy).toHaveBeenCalledWith({ + BookTripMOBILEHUB: awsmobileBot, }); + expect.assertions(2); }); - describe('Sending messages to bot', () => { - jest.useFakeTimers(); - test('onComplete callback from `Interactions.onComplete` called with text', async () => { - const curCredSpyOn = jest - .spyOn(Credentials, 'get') - .mockImplementation(() => Promise.resolve({ identityId: '1234' })); - - function onCompleteCallback(err, confirmation) { - expect(confirmation).toEqual({ slots: { m1: 'hi', m2: 'done' } }); - } - - const configuration = { - Interactions: { - bots: { - BookTripMOBILEHUB: { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - region: 'us-east-1', - }, - }, - }, - }; - - const interactions = new Interactions({}); - - const config = interactions.configure(configuration); - - expect(config).toEqual(configuration.Interactions); - interactions.onComplete('BookTripMOBILEHUB', onCompleteCallback); - await interactions.send('BookTripMOBILEHUB', 'hi'); - const response = await interactions.send('BookTripMOBILEHUB', 'done'); - expect(response).toEqual({ - dialogState: 'ReadyForFulfillment', - message: 'echo:done', - slots: { - m1: 'hi', - m2: 'done', - }, - }); - - const interactionsMessageText = { - content: 'done', - options: { - messageType: 'text', - }, - }; - - const textResponse = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageText - ); - expect(textResponse).toEqual({ - dialogState: 'ReadyForFulfillment', - message: 'echo:done', - slots: { - m1: 'hi', - m2: 'done', - }, - audioStream: new Uint8Array(), - }); - jest.runAllTimers(); - }); - - test('onComplete callback from `Interactions.onComplete` called with voice', async () => { - const curCredSpyOn = jest - .spyOn(Credentials, 'get') - .mockImplementation(() => Promise.resolve({ identityId: '1234' })); - - function onCompleteCallback(err, confirmation) { - expect(confirmation).toEqual({ - slots: { m1: 'voice:hi', m2: 'voice:done' }, - }); - } - - const configuration = { - Interactions: { - bots: { - BookTripMOBILEHUB: { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - region: 'us-east-1', - }, + test('Configure bot belonging to non-existing plugin', async () => { + const myConfig = { + Interactions: { + bots: { + MyBot: { + name: 'MyBot', + alias: '$LATEST', + region: 'us-west-2', + providerName: 'randomProvider', }, }, - }; + }, + }; - const interactions = new Interactions({}); - const config = interactions.configure(configuration); - interactions.onComplete('BookTripMOBILEHUB', onCompleteCallback); + // configuring a bot to a plugin that isn't added yet is allowed + // when the plugin is added the bots belonging to plugin are automatically configured + expect(() => interactions.configure(myConfig)).not.toThrow(); + expect.assertions(1); + }); + }); - const interactionsMessageVoice = { - content: 'voice:done', - options: { - messageType: 'voice', - }, - }; - - const voiceResponse = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageVoice - ); - expect(voiceResponse).toEqual({ - dialogState: 'ReadyForFulfillment', - message: 'voice:echo:voice:done', - slots: { - m1: 'voice:hi', - m2: 'voice:done', - }, - audioStream: new Uint8Array(), - }); - jest.runAllTimers(); - }); + // Test 'getModuleName' API + test(`Is provider name 'Interactions'`, () => { + const interactions = new Interactions({}); + const moduleName = interactions.getModuleName(); + expect(moduleName).toEqual('Interactions'); + expect.assertions(1); + }); - test('onComplete callback from configure being called with text', async () => { - const curCredSpyOn = jest - .spyOn(Credentials, 'get') - .mockImplementation(() => Promise.resolve({ identityId: '1234' })); - - function onCompleteCallback(err, confirmation) { - expect(confirmation).toEqual({ slots: { m1: 'hi', m2: 'done' } }); - } - const configuration = { - Interactions: { - bots: { - BookTripMOBILEHUB: { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - region: 'us-east-1', - onComplete: onCompleteCallback, - }, - }, - }, - }; + // Test 'addPluggable' API + describe('addPluggable API', () => { + let interactions; + let providerConfigureSpy; - const interactions = new Interactions({}); - const config = interactions.configure(configuration); + beforeEach(() => { + interactions = new Interactions({}); + providerConfigureSpy = jest.spyOn(DummyProvider.prototype, 'configure'); + interactions.configure({}); + }); - expect(config).toEqual(configuration.Interactions); + test('Add custom pluggable and configure a bot for that plugin successfully', async () => { + // first add custom plugin + // then configure bots for that plugin + expect(() => + interactions.addPluggable(new DummyProvider()) + ).not.toThrow(); - await interactions.send('BookTripMOBILEHUB', 'hi'); - const response = await interactions.send('BookTripMOBILEHUB', 'done'); - expect(response).toEqual({ - dialogState: 'ReadyForFulfillment', - message: 'echo:done', - slots: { - m1: 'hi', - m2: 'done', - }, - }); - const interactionsMessageText = { - content: 'done', - options: { - messageType: 'text', - }, - }; - - const textResponse = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageText - ); - expect(textResponse).toEqual({ - dialogState: 'ReadyForFulfillment', - message: 'echo:done', - slots: { - m1: 'hi', - m2: 'done', - }, - audioStream: new Uint8Array(), - }); - jest.runAllTimers(); + const config = interactions.configure(manualConfig); + expect(config).toEqual({ + bots: manualConfigBots, }); - test('onComplete callback from configure being called with voice', async () => { - const curCredSpyOn = jest - .spyOn(Credentials, 'get') - .mockImplementation(() => Promise.resolve({ identityId: '1234' })); - - function onCompleteCallback(err, confirmation) { - expect(confirmation).toEqual({ - slots: { m1: 'voice:hi', m2: 'voice:done' }, - }); - } - const configuration = { - Interactions: { - bots: { - BookTripMOBILEHUB: { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - region: 'us-east-1', - onComplete: onCompleteCallback, - }, - }, - }, - }; - - const interactions = new Interactions({}); - const config = interactions.configure(configuration); - - expect(config).toEqual(configuration.Interactions); - - const interactionsMessageVoice = { - content: 'voice:done', - options: { - messageType: 'voice', - }, - }; - const voiceResponse = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageVoice - ); - expect(voiceResponse).toEqual({ - dialogState: 'ReadyForFulfillment', - message: 'voice:echo:voice:done', - slots: { - m1: 'voice:hi', - m2: 'voice:done', - }, - audioStream: new Uint8Array(), - }); - jest.runAllTimers(); + // provider's config get's called for each bot + expect(providerConfigureSpy).toHaveBeenCalledWith({ + BookTrip: manualConfigBots.BookTrip, }); + expect(providerConfigureSpy).toHaveBeenCalledWith({ + OrderFlowers: manualConfigBots.OrderFlowers, + }); + expect.assertions(4); + }); - test('aws-exports configuration and send message to not existing bot', async () => { - const awsmobile = { - aws_bots: 'enable', - aws_bots_config: [ - { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - description: 'Bot to make reservations for a visit to a city.', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', - }; - const interactions = new Interactions({}); - - const config = interactions.configure(awsmobile); - - expect(config).toEqual({ - aws_bots: 'enable', - aws_bots_config: [ - { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', - bots: { - BookTripMOBILEHUB: { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, - }, - }); - - try { - await interactions.send('BookTrip', 'hi'); - } catch (err) { - expect(err.message).toEqual('Bot BookTrip does not exist'); - } + test('Configure bot belonging to custom plugin first, then add pluggable for that bot', async () => { + // first configure bots for a custom plugin + // then add the custom plugin + // when the plugin is added the bots belonging to plugin are automatically configured + const config = interactions.configure(manualConfig); + expect(config).toEqual({ + bots: manualConfigBots, }); - test('aws-exports configuration and try to add onComplete to not existing bot', async () => { - const awsmobile = { - aws_bots: 'enable', - aws_bots_config: [ - { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - description: 'Bot to make reservations for a visit to a city.', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', - }; - const interactions = new Interactions({}); - - const config = interactions.configure(awsmobile); - - expect(config).toEqual({ - aws_bots: 'enable', - aws_bots_config: [ - { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, - ], - aws_project_name: 'bots', - aws_project_region: 'us-east-1', - bots: { - BookTripMOBILEHUB: { - alias: '$LATEST', - 'bot-template': 'bot-trips', - 'commands-help': [ - 'Book a car', - 'Reserve a car', - 'Make a car reservation', - 'Book a hotel', - 'Reserve a room', - 'I want to make a hotel reservation', - ], - description: 'Bot to make reservations for a visit to a city.', - name: 'BookTripMOBILEHUB', - region: 'us-east-1', - }, - }, - }); + expect(() => + interactions.addPluggable(new DummyProvider()) + ).not.toThrow(); - try { - await interactions.onComplete('BookTrip', () => {}); - } catch (err) { - expect(err.message).toEqual('Bot BookTrip does not exist'); - } + // after adding pluggin provider's config get's called for each bot + expect(providerConfigureSpy).toHaveBeenCalledWith({ + BookTrip: manualConfigBots.BookTrip, + }); + expect(providerConfigureSpy).toHaveBeenCalledWith({ + OrderFlowers: manualConfigBots.OrderFlowers, }); + expect.assertions(4); }); - describe('Adding pluggins', () => { - test('Adding AWSLexProvider2 bot not found', async () => { - const configuration = { - Interactions: { - bots: { - BookTripMOBILEHUB: { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - region: 'us-east-1', - providerName: 'AWSLexProvider2', - }, - }, - }, - }; - - const interactions = new Interactions({}); - - const config = interactions.configure(configuration); - - interactions.addPluggable(new AWSLexProvider2()); - - try { - await interactions.send('BookTrip', 'hi'); - } catch (err) { - expect(err.message).toEqual('Bot BookTrip does not exist'); - } - }); + test('Add existing pluggable again', () => { + interactions.addPluggable(new DummyProvider()); + expect(() => { + interactions.addPluggable(new DummyProvider()); + }).toThrow('Pluggable DummyProvider already plugged'); + expect.assertions(1); + }); + }); - test('Adding custom plugin happy path', async () => { - jest - .spyOn(Credentials, 'get') - .mockImplementation(() => Promise.resolve({ identityId: '1234' })); - const configuration = { - Interactions: { - bots: { - BookTripMOBILEHUB: { - name: 'BookTripMOBILEHUB', - alias: '$LATEST', - region: 'us-east-1', - providerName: 'AWSLexProvider2', - }, - }, - }, - }; + // Test 'send' API + describe('send API', () => { + let interactions; + let providerSend; + + beforeEach(() => { + interactions = new Interactions({}); + interactions.configure({}); + interactions.addPluggable(new DummyProvider()); + interactions.configure(manualConfig); + providerSend = jest.spyOn(DummyProvider.prototype, 'sendMessage'); + }); - const interactions = new Interactions({}); - const config = interactions.configure(configuration); - expect(config).toEqual({ - bots: { - BookTripMOBILEHUB: { - alias: '$LATEST', - name: 'BookTripMOBILEHUB', - providerName: 'AWSLexProvider2', - region: 'us-east-1', - }, - }, - }); - const pluggin = new AWSLexProvider2({}); + test('send text message to a bot successfully', async () => { + const response = await interactions.send('BookTrip', 'hi'); + expect(response).toEqual(sampleSendResponse); - interactions.addPluggable(pluggin); + // check if provider's send was called + expect(providerSend).toBeCalledTimes(1); + expect(providerSend).toHaveBeenCalledWith('BookTrip', 'hi'); + expect.assertions(3); + }); - const response = await interactions.send('BookTripMOBILEHUB', 'hi'); + test('Send text message to non-existing bot', async () => { + await expect(interactions.send('unknownBot', 'hi')).rejects.toEqual( + 'Bot unknownBot does not exist' + ); + expect.assertions(1); + }); + }); - expect(response).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi', - }); + // Test 'onComplete' API + describe('onComplete API', () => { + let interactions; + let providerOnComplete; + const callback = (err, confirmation) => {}; + + beforeEach(() => { + interactions = new Interactions({}); + interactions.configure({}); + interactions.addPluggable(new DummyProvider()); + interactions.configure(manualConfig); + providerOnComplete = jest.spyOn(DummyProvider.prototype, 'onComplete'); + }); - const interactionsMessageVoice = { - content: 'voice:hi', - options: { - messageType: 'voice', - }, - }; + test('Configure onComplete callback for a configured bot successfully', async () => { + expect(() => interactions.onComplete('BookTrip', callback)).not.toThrow(); + // check if provider's onComplete was called + expect(providerOnComplete).toBeCalledTimes(1); + expect(providerOnComplete).toHaveBeenCalledWith('BookTrip', callback); + expect.assertions(3); + }); - const interactionsMessageText = { - content: 'hi', - options: { - messageType: 'text', - }, - }; - - const responseVoice = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageVoice - ); - expect(responseVoice).toEqual({ - dialogState: 'ElicitSlot', - message: 'voice:echo:voice:hi', - audioStream: new Uint8Array(), - }); - - const responseText = await interactions.send( - 'BookTripMOBILEHUB', - interactionsMessageText - ); - expect(responseText).toEqual({ - dialogState: 'ElicitSlot', - message: 'echo:hi', - audioStream: new Uint8Array(), - }); - }); + test('Configure onComplete callback for non-existing bot', async () => { + expect(() => interactions.onComplete('unknownBot', callback)).toThrow( + 'Bot unknownBot does not exist' + ); + expect.assertions(1); }); }); }); diff --git a/packages/interactions/__tests__/providers/AWSLexProvider-unit-test.ts b/packages/interactions/__tests__/providers/AWSLexProvider-unit-test.ts new file mode 100644 index 00000000000..3df97d856be --- /dev/null +++ b/packages/interactions/__tests__/providers/AWSLexProvider-unit-test.ts @@ -0,0 +1,475 @@ +/* + * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { AWSLexProvider } from '../../src/Providers'; +import { Credentials } from '@aws-amplify/core'; +import { + LexRuntimeServiceClient, + PostContentCommand, + PostTextCommand, + PostTextCommandOutput, +} from '@aws-sdk/client-lex-runtime-service'; + +(global as any).Response = () => {}; +(global as any).Response.prototype.arrayBuffer = (blob: Blob) => { + return Promise.resolve(new ArrayBuffer(0)); +}; + +// mock stream response +const createBlob = () => { + return new Blob(); +}; + +// bot config +const botConfig = { + BookTrip: { + name: 'BookTrip', // default provider 'AWSLexProvider' + alias: '$LATEST', + region: 'us-west-2', + }, + OrderFlowers: { + name: 'OrderFlowers', + alias: '$LATEST', + region: 'us-west-2', + providerName: 'AWSLexProvider', + }, +}; + +LexRuntimeServiceClient.prototype.send = jest.fn((command, callback) => { + if (command instanceof PostTextCommand) { + if (command.input.inputText === 'done') { + const result = { + message: 'echo:' + command.input.inputText, + dialogState: 'ReadyForFulfillment', + slots: { + m1: 'hi', + m2: 'done', + }, + }; + return Promise.resolve(result); + } else if (command.input.inputText === 'error') { + const result = { + message: 'echo:' + command.input.inputText, + dialogState: 'Failed', + }; + return Promise.resolve(result); + } else { + const result = { + message: 'echo:' + command.input.inputText, + dialogState: 'ElicitSlot', + }; + return Promise.resolve(result); + } + } else if (command instanceof PostContentCommand) { + if ( + command.input.contentType === + 'audio/x-l16; sample-rate=16000; channel-count=1' + ) { + const bot = command.input.botName as string; + const [botName, status] = bot.split(':'); + + if (status === 'done') { + // we add the status to the botName + // because inputStream would just be a blob if type is voice + const result = { + message: 'voice:echo:' + command.input.botName, + dialogState: 'ReadyForFulfillment', + slots: { + m1: 'voice:hi', + m2: 'voice:done', + }, + audioStream: createBlob(), + }; + return Promise.resolve(result); + } else if (status === 'error') { + const result = { + message: 'voice:echo:' + command.input.botName, + dialogState: 'Failed', + audioStream: createBlob(), + }; + return Promise.resolve(result); + } else { + const result = { + message: 'voice:echo:' + command.input.botName, + dialogState: 'ElicitSlot', + audioStream: createBlob(), + }; + return Promise.resolve(result); + } + } else { + if (command.input.inputStream === 'done') { + const result = { + message: 'echo:' + command.input.inputStream, + dialogState: 'ReadyForFulfillment', + slots: { + m1: 'hi', + m2: 'done', + }, + audioStream: createBlob(), + }; + return Promise.resolve(result); + } else if (command.input.inputStream === 'error') { + const result = { + message: 'echo:' + command.input.inputStream, + dialogState: 'Failed', + audioStream: createBlob(), + }; + return Promise.resolve(result); + } else { + const result = { + message: 'echo:' + command.input.inputStream, + dialogState: 'ElicitSlot', + audioStream: createBlob(), + }; + return Promise.resolve(result); + } + } + } +}) as any; + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('Interactions', () => { + // Test 'getProviderName' API + test(`Is provider name 'AWSLexProvider'`, () => { + const provider = new AWSLexProvider(); + expect(provider.getProviderName()).toEqual('AWSLexProvider'); + expect.assertions(1); + }); + + // Test 'getCategory' API + test(`Is category name 'Interactions'`, () => { + const provider = new AWSLexProvider(); + expect(provider.getCategory()).toEqual('Interactions'); + expect.assertions(1); + }); + + // Test 'configure' API + describe('configure API', () => { + const provider = new AWSLexProvider(); + + test('Check if bot is successfully configured by validating config response', () => { + expect(provider.configure(botConfig)).toEqual(botConfig); + expect.assertions(1); + }); + + test('configure multiple bots and re-configure existing bot successfully', () => { + // config 1st bot + expect(provider.configure(botConfig)).toEqual(botConfig); + + const anotherBot = { + BookHotel: { + name: 'BookHotel', + alias: '$LATEST', + region: 'us-west-2', + }, + }; + // config 2nd bot + expect(provider.configure(anotherBot)).toEqual({ + ...botConfig, + ...anotherBot, + }); + + const anotherBotUpdated = { + BookHotel: { + name: 'BookHotel', + alias: 'MyBookHotel', + region: 'us-west-1', + }, + }; + // re-configure updated 2nd bot + // 2nd bot is overridden + expect(provider.configure(anotherBotUpdated)).toEqual({ + ...botConfig, + ...anotherBotUpdated, + }); + expect.assertions(3); + }); + + test('Configure bot with invalid config', () => { + const invalidConfig = { + BookHotel: { + name: 'BookHotel', + region: 'us-west-2', + // alias: '$LATEST', this is required + }, + }; + // @ts-ignore + expect(() => provider.configure(invalidConfig)).toThrow( + 'invalid bot configuration' + ); + expect.assertions(1); + }); + }); + + // Test 'send' API + describe('send API', () => { + let provider; + + beforeEach(() => { + jest + .spyOn(Credentials, 'get') + .mockImplementation(() => Promise.resolve({ identityId: '1234' })); + + provider = new AWSLexProvider(); + provider.configure(botConfig); + }); + + test('send simple text message to bot and fulfill', async () => { + let response = await provider.sendMessage('BookTrip', 'hi'); + expect(response).toEqual({ + dialogState: 'ElicitSlot', + message: 'echo:hi', + }); + + response = await provider.sendMessage('BookTrip', 'done'); + expect(response).toEqual({ + dialogState: 'ReadyForFulfillment', + message: 'echo:done', + slots: { + m1: 'hi', + m2: 'done', + }, + }); + expect.assertions(2); + }); + + test('send obj text message to bot and fulfill', async () => { + let response = await provider.sendMessage('BookTrip', { + content: 'hi', + options: { + messageType: 'text', + }, + }); + expect(response).toEqual({ + dialogState: 'ElicitSlot', + message: 'echo:hi', + audioStream: new Uint8Array(), + }); + + response = await provider.sendMessage('BookTrip', { + content: 'done', + options: { + messageType: 'text', + }, + }); + expect(response).toEqual({ + dialogState: 'ReadyForFulfillment', + message: 'echo:done', + audioStream: new Uint8Array(), + slots: { + m1: 'hi', + m2: 'done', + }, + }); + expect.assertions(2); + }); + + test('send obj voice message to bot and fulfill', async () => { + const botconfig = { + 'BookTrip:hi': { + name: 'BookTrip:hi', + alias: '$LATEST', + region: 'us-west-2', + }, + 'BookTrip:done': { + name: 'BookTrip:done', + alias: '$LATEST', + region: 'us-west-2', + }, + }; + provider.configure(botconfig); + + let response = await provider.sendMessage('BookTrip:hi', { + content: createBlob(), + options: { + messageType: 'voice', + }, + }); + expect(response).toEqual({ + dialogState: 'ElicitSlot', + message: 'voice:echo:BookTrip:hi', + audioStream: new Uint8Array(), + }); + + response = await provider.sendMessage('BookTrip:done', { + content: createBlob(), + options: { + messageType: 'voice', + }, + }); + expect(response).toEqual({ + dialogState: 'ReadyForFulfillment', + message: 'voice:echo:BookTrip:done', + audioStream: new Uint8Array(), + slots: { + m1: 'voice:hi', + m2: 'voice:done', + }, + }); + expect.assertions(2); + }); + + test('send a text message bot But with no credentials', async () => { + jest + .spyOn(Credentials, 'get') + .mockImplementation(() => Promise.reject({ identityId: undefined })); + + await expect(provider.sendMessage('BookTrip', 'hi')).rejects.toEqual( + 'No credentials' + ); + expect.assertions(1); + }); + + test('send message to non-existing bot', async () => { + await expect(provider.sendMessage('unknownBot', 'hi')).rejects.toEqual( + 'Bot unknownBot does not exist' + ); + expect.assertions(1); + }); + }); + + // Test 'onComplete' API + describe('onComplete API', () => { + const callback = (err, confirmation) => {}; + let provider; + + beforeEach(() => { + jest + .spyOn(Credentials, 'get') + .mockImplementation(() => Promise.resolve({ identityId: '1234' })); + + provider = new AWSLexProvider(); + provider.configure(botConfig); + }); + + test('Configure onComplete callback for a configured bot successfully', () => { + expect(() => provider.onComplete('BookTrip', callback)).not.toThrow(); + expect.assertions(1); + }); + + test('Configure onComplete callback for non-existing bot', async () => { + expect(() => provider.onComplete('unknownBot', callback)).toThrow( + 'Bot unknownBot does not exist' + ); + expect.assertions(1); + }); + }); + + // Test 'reportBotStatus' API + describe('reportBotStatus API', () => { + jest.useFakeTimers(); + let provider; + + let inProgressResp; + let completeSuccessResp; + let completeFailResp; + + let inProgressCallback; + let completeSuccessCallback; + let completeFailCallback; + + beforeEach(async () => { + jest + .spyOn(Credentials, 'get') + .mockImplementation(() => Promise.resolve({ identityId: '1234' })); + + provider = new AWSLexProvider(); + provider.configure(botConfig); + + // mock callbacks + inProgressCallback = jest.fn((err, confirmation) => + fail(`callback shouldn't be called`) + ); + + completeSuccessCallback = jest.fn((err, confirmation) => { + expect(err).toEqual(null); + expect(confirmation).toEqual({ slots: { m1: 'hi', m2: 'done' } }); + }); + + completeFailCallback = jest.fn((err, confirmation) => + expect(err).toEqual('Bot conversation failed') + ); + + // mock responses + inProgressResp = (await provider.sendMessage( + 'BookTrip', + 'hi' + )) as PostTextCommandOutput; + + completeSuccessResp = (await provider.sendMessage( + 'BookTrip', + 'done' + )) as PostTextCommandOutput; + + completeFailResp = (await provider.sendMessage( + 'BookTrip', + 'error' + )) as PostTextCommandOutput; + }); + + test('Configure onComplete callback using `Interactions.onComplete` API', async () => { + // 1. In progress, callback shouldn't be called + provider.onComplete('BookTrip', inProgressCallback); + provider.reportBotStatus(inProgressResp, 'BookTrip'); + jest.runAllTimers(); + expect(inProgressCallback).toBeCalledTimes(0); + + // 2. task complete; success, callback be called with response + provider.onComplete('BookTrip', completeSuccessCallback); + provider.reportBotStatus(completeSuccessResp, 'BookTrip'); + jest.runAllTimers(); + expect(completeSuccessCallback).toBeCalledTimes(1); + + // 3. task complete; error, callback be called with error + provider.onComplete('BookTrip', completeFailCallback); + provider.reportBotStatus(completeFailResp, 'BookTrip'); + jest.runAllTimers(); + expect(completeFailCallback).toBeCalledTimes(1); + expect.assertions(6); + }); + + test('Configure onComplete callback using `configuration`', async () => { + const myBot: any = { + BookTrip: { + name: 'BookTrip', + alias: '$LATEST', + region: 'us-west-2', + }, + }; + + // 1. In progress, callback shouldn't be called + myBot.BookTrip.onComplete = inProgressCallback; + provider.configure(myBot); + provider.reportBotStatus(inProgressResp, 'BookTrip'); + jest.runAllTimers(); + expect(inProgressCallback).toBeCalledTimes(0); + + // 2. In progress, callback shouldn't be called + myBot.BookTrip.onComplete = completeSuccessCallback; + provider.configure(myBot); + provider.reportBotStatus(completeSuccessResp, 'BookTrip'); + jest.runAllTimers(); + expect(completeSuccessCallback).toBeCalledTimes(1); + + // 3. In progress, callback shouldn't be called + myBot.BookTrip.onComplete = completeFailCallback; + provider.configure(myBot); + provider.reportBotStatus(completeFailResp, 'BookTrip'); + jest.runAllTimers(); + expect(completeFailCallback).toBeCalledTimes(1); + expect.assertions(6); + }); + }); +}); diff --git a/packages/interactions/package.json b/packages/interactions/package.json index 2e2271b39e7..32487da8182 100644 --- a/packages/interactions/package.json +++ b/packages/interactions/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/interactions", - "version": "4.0.38", + "version": "4.0.51", "description": "Interactions category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -41,7 +41,7 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/core": "4.5.2", + "@aws-amplify/core": "4.7.2", "@aws-sdk/client-lex-runtime-service": "3.6.1" }, "jest": { diff --git a/packages/interactions/src/Interactions.ts b/packages/interactions/src/Interactions.ts index 6d64d0beb76..dbd39c21713 100644 --- a/packages/interactions/src/Interactions.ts +++ b/packages/interactions/src/Interactions.ts @@ -31,7 +31,7 @@ export class InteractionsClass { * * @param {InteractionsOptions} options - Configuration object for Interactions */ - constructor(options: InteractionsOptions) { + constructor(options: InteractionsOptions = {}) { this._options = options; logger.debug('Interactions Options', this._options); this._pluggables = {}; @@ -44,9 +44,9 @@ export class InteractionsClass { /** * * @param {InteractionsOptions} options - Configuration object for Interactions - * @return {Object} - The current configuration + * @return {InteractionsOptions} - The current configuration */ - configure(options: InteractionsOptions) { + public configure(options: InteractionsOptions): InteractionsOptions { const opt = options ? options.Interactions || options : {}; logger.debug('configure Interactions', { opt }); this._options = { bots: {}, ...opt, ...opt.Interactions }; @@ -63,19 +63,27 @@ export class InteractionsClass { } } - // Check if AWSLex provider is already on pluggables - if ( - !this._pluggables.AWSLexProvider && - bots_config && - Object.keys(bots_config) - .map(key => bots_config[key]) - .find(bot => !bot.providerName || bot.providerName === 'AWSLexProvider') - ) { - this._pluggables.AWSLexProvider = new AWSLexProvider(); - } + // configure bots to their specific providers + Object.keys(bots_config).forEach(botKey => { + const bot = bots_config[botKey]; + const providerName = bot.providerName || 'AWSLexProvider'; + + // add default provider if required + if ( + !this._pluggables.AWSLexProvider && + providerName === 'AWSLexProvider' + ) { + this._pluggables.AWSLexProvider = new AWSLexProvider(); + } - Object.keys(this._pluggables).map(key => { - this._pluggables[key].configure(this._options.bots); + // configure bot with it's respective provider + if (this._pluggables[providerName]) { + this._pluggables[providerName].configure({ [bot.name]: bot }); + } else { + logger.debug( + `bot ${bot.name} was not configured as ${providerName} provider was not found` + ); + } }); return this._options; @@ -84,12 +92,23 @@ export class InteractionsClass { public addPluggable(pluggable: InteractionsProvider) { if (pluggable && pluggable.getCategory() === 'Interactions') { if (!this._pluggables[pluggable.getProviderName()]) { - pluggable.configure(this._options.bots); + // configure bots for the new plugin + Object.keys(this._options.bots) + .filter( + botKey => + this._options.bots[botKey].providerName === + pluggable.getProviderName() + ) + .forEach(botKey => { + const bot = this._options.bots[botKey]; + pluggable.configure({ [bot.name]: bot }); + }); + this._pluggables[pluggable.getProviderName()] = pluggable; return; } else { throw new Error( - 'Bot ' + pluggable.getProviderName() + ' already plugged' + 'Pluggable ' + pluggable.getProviderName() + ' already plugged' ); } } @@ -112,14 +131,14 @@ export class InteractionsClass { message: string | object ): Promise { if (!this._options.bots || !this._options.bots[botname]) { - throw new Error('Bot ' + botname + ' does not exist'); + return Promise.reject('Bot ' + botname + ' does not exist'); } const botProvider = this._options.bots[botname].providerName || 'AWSLexProvider'; if (!this._pluggables[botProvider]) { - throw new Error( + return Promise.reject( 'Bot ' + botProvider + ' does not have valid pluggin did you try addPluggable first?' @@ -128,7 +147,10 @@ export class InteractionsClass { return await this._pluggables[botProvider].sendMessage(botname, message); } - public onComplete(botname: string, callback: (err, confirmation) => void) { + public onComplete( + botname: string, + callback: (err, confirmation) => void + ): void { if (!this._options.bots || !this._options.bots[botname]) { throw new Error('Bot ' + botname + ' does not exist'); } @@ -146,5 +168,5 @@ export class InteractionsClass { } } -export const Interactions = new InteractionsClass(null); +export const Interactions = new InteractionsClass(); Amplify.register(Interactions); diff --git a/packages/interactions/src/Providers/AWSLexProvider.ts b/packages/interactions/src/Providers/AWSLexProvider.ts index 877bd4bbc74..e7621e7268e 100644 --- a/packages/interactions/src/Providers/AWSLexProvider.ts +++ b/packages/interactions/src/Providers/AWSLexProvider.ts @@ -10,17 +10,21 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ - import { AbstractInteractionsProvider } from './InteractionsProvider'; import { InteractionsOptions, + AWSLexProviderOptions, InteractionsResponse, InteractionsMessage, } from '../types'; import { LexRuntimeServiceClient, PostTextCommand, + PostTextCommandInput, + PostTextCommandOutput, PostContentCommand, + PostContentCommandInput, + PostContentCommandOutput, } from '@aws-sdk/client-lex-runtime-service'; import { ConsoleLogger as Logger, @@ -44,7 +48,24 @@ export class AWSLexProvider extends AbstractInteractionsProvider { return 'AWSLexProvider'; } - reportBotStatus(data, botname) { + configure(config: AWSLexProviderOptions = {}): AWSLexProviderOptions { + const propertiesToTest = ['name', 'alias', 'region']; + + Object.keys(config).forEach(botKey => { + const botConfig = config[botKey]; + + // is bot config correct + if (!propertiesToTest.every(x => x in botConfig)) { + throw new Error('invalid bot configuration'); + } + }); + return super.configure(config); + } + + reportBotStatus( + data: PostTextCommandOutput | PostContentCommandOutput, + botname: string + ) { // Check if state is fulfilled to resolve onFullfilment promise logger.debug('postContent state', data.dialogState); if ( @@ -94,11 +115,16 @@ export class AWSLexProvider extends AbstractInteractionsProvider { botname: string, message: string | InteractionsMessage ): Promise { + // check if bot exists if (!this._config[botname]) { return Promise.reject('Bot ' + botname + ' does not exist'); } - const credentials = await Credentials.get(); - if (!credentials) { + + // check if credentials are present + let credentials; + try { + credentials = await Credentials.get(); + } catch (error) { return Promise.reject('No credentials'); } @@ -108,7 +134,7 @@ export class AWSLexProvider extends AbstractInteractionsProvider { customUserAgent: getAmplifyUserAgent(), }); - let params; + let params: PostTextCommandInput | PostContentCommandInput; if (typeof message === 'string') { params = { botAlias: this._config[botname].alias, @@ -118,10 +144,10 @@ export class AWSLexProvider extends AbstractInteractionsProvider { }; logger.debug('postText to lex', message); - try { const postTextCommand = new PostTextCommand(params); const data = await this.lexRuntimeServiceClient.send(postTextCommand); + this.reportBotStatus(data, botname); return data; } catch (err) { @@ -133,15 +159,21 @@ export class AWSLexProvider extends AbstractInteractionsProvider { options: { messageType }, } = message; if (messageType === 'voice') { + if (!(content instanceof Blob || content instanceof ReadableStream)) + return Promise.reject('invalid content type'); + params = { botAlias: this._config[botname].alias, botName: botname, - contentType: 'audio/x-l16; sample-rate=16000', - inputStream: content, + contentType: 'audio/x-l16; sample-rate=16000; channel-count=1', + inputStream: await convert(content), userId: credentials.identityId, accept: 'audio/mpeg', }; } else { + if (typeof content !== 'string') + return Promise.reject('invalid content type'); + params = { botAlias: this._config[botname].alias, botName: botname, @@ -157,7 +189,11 @@ export class AWSLexProvider extends AbstractInteractionsProvider { const data = await this.lexRuntimeServiceClient.send( postContentCommand ); - const audioArray = await convert(data.audioStream); + + const audioArray = data.audioStream + ? await convert(data.audioStream) + : undefined; + this.reportBotStatus(data, botname); return { ...data, ...{ audioStream: audioArray } }; } catch (err) { @@ -166,9 +202,10 @@ export class AWSLexProvider extends AbstractInteractionsProvider { } } - onComplete(botname: string, callback) { + onComplete(botname: string, callback: (err, confirmation) => void) { + // does bot exist if (!this._config[botname]) { - throw new ErrorEvent('Bot ' + botname + ' does not exist'); + throw new Error('Bot ' + botname + ' does not exist'); } this._botsCompleteCallback[botname] = callback; } diff --git a/packages/interactions/src/index.ts b/packages/interactions/src/index.ts index 06d7057e430..966e658e38a 100644 --- a/packages/interactions/src/index.ts +++ b/packages/interactions/src/index.ts @@ -18,6 +18,7 @@ import { Interactions } from './Interactions'; export default Interactions; export * from './types'; +export * from './Providers/InteractionsProvider'; export * from './Providers/AWSLexProvider'; export { Interactions }; diff --git a/packages/interactions/src/types/Provider.ts b/packages/interactions/src/types/Provider.ts index 23a056bd91c..1fcbe504095 100644 --- a/packages/interactions/src/types/Provider.ts +++ b/packages/interactions/src/types/Provider.ts @@ -15,7 +15,7 @@ import { InteractionsResponse } from './Response'; export interface InteractionsProvider { // configure your provider - configure(config: object): object; + configure(config: InteractionsOptions): InteractionsOptions; // return 'Interactions' getCategory(): string; diff --git a/packages/interactions/src/types/Providers/AWSLexProvider.ts b/packages/interactions/src/types/Providers/AWSLexProvider.ts new file mode 100644 index 00000000000..dc18eb66de6 --- /dev/null +++ b/packages/interactions/src/types/Providers/AWSLexProvider.ts @@ -0,0 +1,11 @@ +export interface AWSLexProviderOption { + name: string; + alias: string; + region: string; + providerName?: string; + onComplete?(botname: string, callback: (err, confirmation) => void): void; +} + +export interface AWSLexProviderOptions { + [key: string]: AWSLexProviderOption; +} diff --git a/packages/interactions/src/types/index.ts b/packages/interactions/src/types/index.ts index c3c9ffa9733..4f454b615a8 100644 --- a/packages/interactions/src/types/index.ts +++ b/packages/interactions/src/types/index.ts @@ -12,4 +12,5 @@ */ export * from './Interactions'; export * from './Provider'; +export * from './Providers/AWSLexProvider'; export * from './Response'; diff --git a/packages/predictions/CHANGELOG.md b/packages/predictions/CHANGELOG.md index 7ab1c5a351e..f6435c117d7 100644 --- a/packages/predictions/CHANGELOG.md +++ b/packages/predictions/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.50...@aws-amplify/predictions@4.0.51) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.49...@aws-amplify/predictions@4.0.50) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.48...@aws-amplify/predictions@4.0.49) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.47...@aws-amplify/predictions@4.0.48) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.46...@aws-amplify/predictions@4.0.47) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.45...@aws-amplify/predictions@4.0.46) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.44...@aws-amplify/predictions@4.0.45) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.43...@aws-amplify/predictions@4.0.44) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.42...@aws-amplify/predictions@4.0.43) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.41...@aws-amplify/predictions@4.0.42) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.40...@aws-amplify/predictions@4.0.41) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.39...@aws-amplify/predictions@4.0.40) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + +## [4.0.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.38...@aws-amplify/predictions@4.0.39) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/predictions + + + + + ## [4.0.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/predictions@4.0.37...@aws-amplify/predictions@4.0.38) (2022-04-14) **Note:** Version bump only for package @aws-amplify/predictions diff --git a/packages/predictions/package.json b/packages/predictions/package.json index c1991945cf5..7098678f100 100644 --- a/packages/predictions/package.json +++ b/packages/predictions/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/predictions", - "version": "4.0.38", + "version": "4.0.51", "description": "Machine learning category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -40,8 +40,8 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/core": "4.5.2", - "@aws-amplify/storage": "4.4.21", + "@aws-amplify/core": "4.7.2", + "@aws-amplify/storage": "4.5.4", "@aws-sdk/client-comprehend": "3.6.1", "@aws-sdk/client-polly": "3.6.1", "@aws-sdk/client-rekognition": "3.6.1", diff --git a/packages/pubsub/CHANGELOG.md b/packages/pubsub/CHANGELOG.md index a64eca534fc..957d6d454ae 100644 --- a/packages/pubsub/CHANGELOG.md +++ b/packages/pubsub/CHANGELOG.md @@ -3,6 +3,124 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.5.0...@aws-amplify/pubsub@4.5.1) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +# [4.5.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.10...@aws-amplify/pubsub@4.5.0) (2022-08-18) + + +### Bug Fixes + +* **pubsub:** Connection Ack verification bug ([#10200](https://github.com/aws-amplify/amplify-js/issues/10200)) ([d6cb7f9](https://github.com/aws-amplify/amplify-js/commit/d6cb7f95d68b05c9668bfd1f823b9db264f6291e)) + + +### Features + +* PubSub Connection state tracking for MQTT and IoT providers ([#10136](https://github.com/aws-amplify/amplify-js/issues/10136)) ([f28918b](https://github.com/aws-amplify/amplify-js/commit/f28918b1ca1111f98c231c8ed6bccace9ad9e607)) + + + + + +## [4.4.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.9...@aws-amplify/pubsub@4.4.10) (2022-08-16) + + +### Bug Fixes + +* **pubsub:** Add distinct RN Reachibility implementation ([#10175](https://github.com/aws-amplify/amplify-js/issues/10175)) ([5f427f3](https://github.com/aws-amplify/amplify-js/commit/5f427f3ae47d76231a81084c216527b7fabd668a)) + + + + + +## [4.4.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.8...@aws-amplify/pubsub@4.4.9) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.7...@aws-amplify/pubsub@4.4.8) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.6...@aws-amplify/pubsub@4.4.7) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.5...@aws-amplify/pubsub@4.4.6) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.4...@aws-amplify/pubsub@4.4.5) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.3...@aws-amplify/pubsub@4.4.4) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.2...@aws-amplify/pubsub@4.4.3) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.1...@aws-amplify/pubsub@4.4.2) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +## [4.4.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.4.0...@aws-amplify/pubsub@4.4.1) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/pubsub + + + + + +# [4.4.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.3.2...@aws-amplify/pubsub@4.4.0) (2022-05-03) + + +### Features + +* **pubsub:** Add test coverage for the AWSAppSyncRealTimeProvider ([#9778](https://github.com/aws-amplify/amplify-js/issues/9778)) ([348366a](https://github.com/aws-amplify/amplify-js/commit/348366a044be2a3364c956f3a59ea125a7fb7d58)) + + + + + ## [4.3.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pubsub@4.3.1...@aws-amplify/pubsub@4.3.2) (2022-04-14) diff --git a/packages/pubsub/__tests__/AWSAppSyncRealTimeProvider.test.ts b/packages/pubsub/__tests__/AWSAppSyncRealTimeProvider.test.ts index 029278c20f3..e0479427bc4 100644 --- a/packages/pubsub/__tests__/AWSAppSyncRealTimeProvider.test.ts +++ b/packages/pubsub/__tests__/AWSAppSyncRealTimeProvider.test.ts @@ -1,11 +1,26 @@ -import { Auth } from '@aws-amplify/auth'; -import { Credentials, Logger, Signer } from '@aws-amplify/core'; -import { GraphQLError, isCompositeType } from 'graphql'; +jest.mock('@aws-amplify/core', () => ({ + __esModule: true, + ...jest.requireActual('@aws-amplify/core'), + browserOrNode() { + return { + isBrowser: true, + isNode: false, + }; + }, +})); + import Observable from 'zen-observable-ts'; -import { AWSAppSyncRealTimeProvider } from '../src/Providers/AWSAppSyncRealTimeProvider'; +import { Reachability, Credentials, Logger, Signer } from '@aws-amplify/core'; +import { Auth } from '@aws-amplify/auth'; import Cache from '@aws-amplify/cache'; -import { MESSAGE_TYPES } from '../src/Providers/AWSAppSyncRealTimeProvider/constants'; -import { FakeWebSocketInterface, delay, replaceConstant } from './helpers'; + +import { MESSAGE_TYPES } from '../src/Providers/constants'; +import * as constants from '../src/Providers/constants'; + +import { delay, FakeWebSocketInterface, replaceConstant } from './helpers'; +import { ConnectionState as CS } from '../src'; + +import { AWSAppSyncRealTimeProvider } from '../src/Providers/AWSAppSyncRealTimeProvider'; describe('AWSAppSyncRealTimeProvider', () => { describe('isCustomDomain()', () => { @@ -76,13 +91,75 @@ describe('AWSAppSyncRealTimeProvider', () => { fakeWebSocketInterface.newWebSocket(); return fakeWebSocketInterface.webSocket; }); + + // Reduce retry delay for tests to 100ms + Object.defineProperty(constants, 'MAX_DELAY_MS', { + value: 100, + }); + + // Set the network to "online" for these tests + const spyon = jest + .spyOn(Reachability.prototype, 'networkMonitor') + .mockImplementationOnce( + () => + new Observable(observer => { + observer.next?.({ online: true }); + }) + ); }); afterEach(async () => { await fakeWebSocketInterface?.closeInterface(); + fakeWebSocketInterface?.teardown(); loggerSpy.mockClear(); }); + test('standard subscription / unsubscription steps through the expected connection states', async () => { + const observer = provider.subscribe('test', { + appSyncGraphqlEndpoint: 'ws://localhost:8080', + }); + + const subscription = observer.subscribe({ + next: () => {}, + error: x => {}, + }); + + // Wait for the socket to be ready + await fakeWebSocketInterface?.standardConnectionHandshake(); + await fakeWebSocketInterface?.sendMessage( + new MessageEvent('start_ack', { + data: JSON.stringify({ + type: MESSAGE_TYPES.GQL_START_ACK, + payload: { connectionTimeoutMs: 100 }, + id: fakeWebSocketInterface?.webSocket.subscriptionId, + }), + }) + ); + + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.Connected, + ]); + expect(fakeWebSocketInterface?.observedConnectionStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + ]); + + subscription.unsubscribe(); + + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.ConnectedPendingDisconnect, + ]); + + expect(fakeWebSocketInterface?.observedConnectionStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + CS.ConnectedPendingDisconnect, + CS.Disconnected, + ]); + }); + test('returns error when no appSyncGraphqlEndpoint is provided', async () => { expect.assertions(2); const mockError = jest.fn(); @@ -117,7 +194,7 @@ describe('AWSAppSyncRealTimeProvider', () => { .subscribe('test', { appSyncGraphqlEndpoint: 'ws://localhost:8080', }) - .subscribe({}); + .subscribe({ error: () => {} }); // Wait for the socket to be initialize await fakeWebSocketInterface.readyForUse; @@ -143,7 +220,7 @@ describe('AWSAppSyncRealTimeProvider', () => { .subscribe('test', { appSyncGraphqlEndpoint: 'http://localhost:8080', }) - .subscribe({}); + .subscribe({ error: () => {} }); // Wait for the socket to be initialize await fakeWebSocketInterface.readyForUse; @@ -170,7 +247,7 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'https://testaccounturl123456789123.appsync-api.us-east-1.amazonaws.com/graphql', }) - .subscribe({}); + .subscribe({ error: () => {} }); // Wait for the socket to be initialize await fakeWebSocketInterface.readyForUse; @@ -184,12 +261,11 @@ describe('AWSAppSyncRealTimeProvider', () => { test('subscription fails when onerror triggered while waiting for onopen', async () => { expect.assertions(1); - provider .subscribe('test', { appSyncGraphqlEndpoint: 'ws://localhost:8080', }) - .subscribe({}); + .subscribe({ error: () => {} }); await fakeWebSocketInterface?.readyForUse; await fakeWebSocketInterface?.triggerError(); expect(loggerSpy).toHaveBeenCalledWith( @@ -205,11 +281,14 @@ describe('AWSAppSyncRealTimeProvider', () => { .subscribe('test', { appSyncGraphqlEndpoint: 'ws://localhost:8080', }) - .subscribe({}); + .subscribe({ error: () => {} }); + await fakeWebSocketInterface?.readyForUse; await fakeWebSocketInterface?.triggerClose(); - await delay(50); + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.Disconnected, + ]); // Watching for raised exception to be caught and logged expect(loggerSpy).toBeCalledWith( 'DEBUG', @@ -222,16 +301,17 @@ describe('AWSAppSyncRealTimeProvider', () => { test('subscription fails when onerror triggered while waiting for handshake', async () => { expect.assertions(1); + await replaceConstant('CONNECTION_INIT_TIMEOUT', 20, async () => { + provider + .subscribe('test', { + appSyncGraphqlEndpoint: 'ws://localhost:8080', + }) + .subscribe({ error: () => {} }); - provider - .subscribe('test', { - appSyncGraphqlEndpoint: 'ws://localhost:8080', - }) - .subscribe({}); - await fakeWebSocketInterface?.readyForUse; - await fakeWebSocketInterface?.triggerOpen(); - await fakeWebSocketInterface?.triggerError(); - + await fakeWebSocketInterface?.readyForUse; + await fakeWebSocketInterface?.triggerOpen(); + await fakeWebSocketInterface?.triggerError(); + }); // When the socket throws an error during handshake expect(loggerSpy).toHaveBeenCalledWith( 'DEBUG', @@ -242,14 +322,17 @@ describe('AWSAppSyncRealTimeProvider', () => { test('subscription fails when onclose triggered while waiting for handshake', async () => { expect.assertions(1); - provider - .subscribe('test', { - appSyncGraphqlEndpoint: 'ws://localhost:8080', - }) - .subscribe({}); - await fakeWebSocketInterface?.readyForUse; - await fakeWebSocketInterface?.triggerOpen(); - await fakeWebSocketInterface?.triggerClose(); + await replaceConstant('CONNECTION_INIT_TIMEOUT', 20, async () => { + provider + .subscribe('test', { + appSyncGraphqlEndpoint: 'ws://localhost:8080', + }) + .subscribe({ error: () => {} }); + + await fakeWebSocketInterface?.readyForUse; + await fakeWebSocketInterface?.triggerOpen(); + await fakeWebSocketInterface?.triggerClose(); + }); // When the socket is closed during handshake // Watching for raised exception to be caught and logged @@ -305,6 +388,7 @@ describe('AWSAppSyncRealTimeProvider', () => { data: JSON.stringify({ type: MESSAGE_TYPES.GQL_START_ACK, payload: { connectionTimeoutMs: 100 }, + id: fakeWebSocketInterface?.webSocket.subscriptionId, }), }) ); @@ -336,6 +420,7 @@ describe('AWSAppSyncRealTimeProvider', () => { data: JSON.stringify({ type: MESSAGE_TYPES.GQL_START_ACK, payload: { connectionTimeoutMs: 100 }, + id: fakeWebSocketInterface?.webSocket.subscriptionId, }), }) ); @@ -473,39 +558,59 @@ describe('AWSAppSyncRealTimeProvider', () => { ); }); - test('subscription observer error is triggered when a connection is formed and an ack data message is received then ack timeout prompts disconnect', async () => { - expect.assertions(1); + test('subscription observer error is triggered when a connection is formed and an ack data message is received then ka timeout prompts disconnect', async () => { + expect.assertions(2); const observer = provider.subscribe('test', { appSyncGraphqlEndpoint: 'ws://localhost:8080', }); - const subscription = observer.subscribe({ - error: () => {}, - }); - - await fakeWebSocketInterface?.readyForUse; - await fakeWebSocketInterface?.triggerOpen(); - + const subscription = observer.subscribe({ error: () => {} }); // Resolve the message delivery actions - await Promise.resolve( - fakeWebSocketInterface?.sendMessage( - new MessageEvent('connection_ack', { - data: JSON.stringify({ - type: MESSAGE_TYPES.GQL_CONNECTION_ACK, - payload: { connectionTimeoutMs: 20 }, - }), - }) - ) + await replaceConstant( + 'DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT', + 5, + async () => { + await fakeWebSocketInterface?.readyForUse; + await fakeWebSocketInterface?.triggerOpen(); + await fakeWebSocketInterface?.sendMessage( + new MessageEvent('connection_ack', { + data: JSON.stringify({ + type: constants.MESSAGE_TYPES.GQL_CONNECTION_ACK, + payload: { connectionTimeoutMs: 100 }, + }), + }) + ); + + await fakeWebSocketInterface?.sendMessage( + new MessageEvent('start_ack', { + data: JSON.stringify({ + type: MESSAGE_TYPES.GQL_START_ACK, + payload: {}, + id: fakeWebSocketInterface?.webSocket.subscriptionId, + }), + }) + ); + + await fakeWebSocketInterface?.sendDataMessage({ + type: MESSAGE_TYPES.GQL_CONNECTION_KEEP_ALIVE, + payload: { data: {} }, + }); + } ); - await fakeWebSocketInterface?.sendDataMessage({ - type: MESSAGE_TYPES.GQL_CONNECTION_KEEP_ALIVE, - payload: { data: {} }, - }); + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.Connected, + ]); - // Now wait for the timeout to elapse - await delay(100); + // Wait until the socket is automatically disconnected + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.ConnectionDisrupted, + ]); + + expect(fakeWebSocketInterface?.observedConnectionStates).toContain( + CS.ConnectedPendingKeepAlive + ); expect(loggerSpy).toBeCalledWith( 'DEBUG', @@ -520,7 +625,7 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', }); - const subscription = observer.subscribe({}); + const subscription = observer.subscribe({ error: () => {} }); await fakeWebSocketInterface?.standardConnectionHandshake(); await fakeWebSocketInterface?.sendDataMessage({ @@ -536,19 +641,19 @@ describe('AWSAppSyncRealTimeProvider', () => { test('failure to ack before timeout', async () => { expect.assertions(1); - await replaceConstant('START_ACK_TIMEOUT', 20, async () => { + await replaceConstant('START_ACK_TIMEOUT', 30, async () => { const observer = provider.subscribe('test', { appSyncGraphqlEndpoint: 'ws://localhost:8080', }); - const subscription = observer.subscribe({ - error: () => {}, - }); + const subscription = observer.subscribe({ error: () => {} }); await fakeWebSocketInterface?.standardConnectionHandshake(); - // Wait long enough that the shortened timeout will elapse - await delay(100); + // Wait until the socket is automatically disconnected + await fakeWebSocketInterface?.waitForConnectionState([ + CS.Disconnected, + ]); expect(loggerSpy).toBeCalledWith( 'DEBUG', @@ -558,7 +663,44 @@ describe('AWSAppSyncRealTimeProvider', () => { }); }); - test('connection init timeout', async () => { + test('connection init timeout met', async () => { + expect.assertions(2); + await replaceConstant('CONNECTION_INIT_TIMEOUT', 20, async () => { + const observer = provider.subscribe('test', { + appSyncGraphqlEndpoint: 'ws://localhost:8080', + }); + + const subscription = observer.subscribe({ error: () => {} }); + + await fakeWebSocketInterface?.readyForUse; + Promise.resolve(); + await fakeWebSocketInterface?.triggerOpen(); + Promise.resolve(); + await fakeWebSocketInterface?.handShakeMessage(); + + // Wait no less than 20 ms + await delay(20); + + // Wait until the socket is automatically disconnected + await expect( + fakeWebSocketInterface?.hubConnectionListener + ?.currentConnectionState + ).toBe(CS.Connecting); + + // Watching for raised exception to be caught and logged + expect(loggerSpy).not.toBeCalledWith( + 'DEBUG', + 'error on bound ', + expect.objectContaining({ + message: expect.stringMatching( + 'Connection timeout: ack from AWSAppSyncRealTime was not received after' + ), + }) + ); + }); + }); + + test('connection init timeout missed', async () => { expect.assertions(1); await replaceConstant('CONNECTION_INIT_TIMEOUT', 20, async () => { @@ -566,15 +708,19 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', }); - const subscription = observer.subscribe({ - error: () => {}, - }); + const subscription = observer.subscribe({ error: () => {} }); await fakeWebSocketInterface?.readyForUse; + Promise.resolve(); await fakeWebSocketInterface?.triggerOpen(); - // Wait long enough that the shortened timeout will elapse - await delay(100); + // Wait no less than 20 ms + await delay(20); + + // Wait until the socket is automatically disconnected + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.Disconnected, + ]); // Watching for raised exception to be caught and logged expect(loggerSpy).toBeCalledWith( @@ -582,7 +728,7 @@ describe('AWSAppSyncRealTimeProvider', () => { 'error on bound ', expect.objectContaining({ message: expect.stringMatching( - 'Connection timeout: ack from AWSRealTime' + 'Connection timeout: ack from AWSAppSyncRealTime was not received after' ), }) ); @@ -598,7 +744,8 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', authenticationType: 'API_KEY', }) - .subscribe({}); + .subscribe({ error: () => {} }); + await fakeWebSocketInterface?.readyForUse; expect(loggerSpy).toBeCalledWith( @@ -626,7 +773,8 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', authenticationType: 'AWS_IAM', }) - .subscribe({}); + .subscribe({ error: () => {} }); + await fakeWebSocketInterface?.readyForUse; expect(loggerSpy).toBeCalledWith( @@ -704,8 +852,10 @@ describe('AWSAppSyncRealTimeProvider', () => { }, }); - // It takes time for the credentials to resolve - await delay(50); + // Wait until the socket is automatically disconnected + await fakeWebSocketInterface?.waitUntilConnectionStateIn([ + CS.Disconnected, + ]); expect(loggerSpy).toHaveBeenCalledWith( 'WARN', @@ -730,7 +880,8 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', authenticationType: 'OPENID_CONNECT', }) - .subscribe({}); + .subscribe({ error: () => {} }); + await fakeWebSocketInterface?.readyForUse; expect(loggerSpy).toBeCalledWith( @@ -785,9 +936,9 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', authenticationType: 'OPENID_CONNECT', }) - .subscribe({}); - await fakeWebSocketInterface?.readyForUse; + .subscribe({ error: () => {} }); + await fakeWebSocketInterface?.readyForUse; expect(loggerSpy).toBeCalledWith( 'DEBUG', 'Authenticating with OPENID_CONNECT' @@ -814,7 +965,8 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', authenticationType: 'AMAZON_COGNITO_USER_POOLS', }) - .subscribe({}); + .subscribe({ error: () => {} }); + await fakeWebSocketInterface?.readyForUse; expect(loggerSpy).toBeCalledWith( @@ -834,7 +986,8 @@ describe('AWSAppSyncRealTimeProvider', () => { Authorization: 'test', }, }) - .subscribe({}); + .subscribe({ error: () => {} }); + await fakeWebSocketInterface?.readyForUse; expect(loggerSpy).toBeCalledWith( @@ -851,7 +1004,7 @@ describe('AWSAppSyncRealTimeProvider', () => { appSyncGraphqlEndpoint: 'ws://localhost:8080', authenticationType: 'AWS_LAMBDA', additionalHeaders: { - Authorization: undefined, + Authorization: '', }, }) .subscribe({ diff --git a/packages/pubsub/__tests__/ConnectionStateMonitor.tests.ts b/packages/pubsub/__tests__/ConnectionStateMonitor.tests.ts new file mode 100644 index 00000000000..fc5d4f53650 --- /dev/null +++ b/packages/pubsub/__tests__/ConnectionStateMonitor.tests.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2017-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +jest.mock('@aws-amplify/core', () => ({ + __esModule: true, + ...jest.requireActual('@aws-amplify/core'), + browserOrNode() { + return { + isBrowser: true, + isNode: false, + }; + }, +})); + +import Observable from 'zen-observable-ts'; +import { Reachability } from '@aws-amplify/core'; +import { + ConnectionStateMonitor, + CONNECTION_CHANGE, +} from '../src/utils/ConnectionStateMonitor'; +import { ConnectionState as CS } from '../src'; + +describe('ConnectionStateMonitor', () => { + let monitor: ConnectionStateMonitor; + let observedStates: CS[]; + let subscription: ZenObservable.Subscription; + let reachabilityObserver: ZenObservable.Observer<{ online: boolean }>; + + beforeEach(() => { + const spyon = jest + .spyOn(Reachability.prototype, 'networkMonitor') + .mockImplementationOnce(() => { + return new Observable(observer => { + reachabilityObserver = observer; + }); + }); + }); + + describe('when the network is connected', () => { + beforeEach(() => { + reachabilityObserver?.next?.({ online: true }); + + observedStates = []; + subscription?.unsubscribe(); + monitor = new ConnectionStateMonitor(); + subscription = monitor.connectionStateObservable.subscribe(value => { + observedStates.push(value); + }); + }); + + test('connection states starts out disconnected', () => { + expect(observedStates).toEqual(['Disconnected']); + }); + + test('standard states connection pattern', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + expect(observedStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + ]); + }); + + test('connection states when the network is lost while connected', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + reachabilityObserver?.next?.({ online: false }); + expect(observedStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + CS.ConnectedPendingNetwork, + ]); + }); + + test('connection states when the network is lost and the connection times out', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + reachabilityObserver?.next?.({ online: false }); + monitor.record(CONNECTION_CHANGE.CLOSED); + expect(observedStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + CS.ConnectedPendingNetwork, + CS.ConnectionDisruptedPendingNetwork, + ]); + }); + + test('connection states when the network is lost, the connection times out and then the network recovers', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + reachabilityObserver?.next?.({ online: false }); + monitor.record(CONNECTION_CHANGE.CLOSED); + reachabilityObserver?.next?.({ online: true }); + expect(observedStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + CS.ConnectedPendingNetwork, + CS.ConnectionDisruptedPendingNetwork, + CS.ConnectionDisrupted, + ]); + }); + + test('connection states when a connection is no longer needed', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + monitor.record(CONNECTION_CHANGE.CLOSING_CONNECTION); + + expect(observedStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + CS.ConnectedPendingDisconnect, + ]); + }); + + test('connection states when a connection is no longer needed closed', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + monitor.record(CONNECTION_CHANGE.CLOSING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CLOSED); + + expect(observedStates).toEqual([ + CS.Disconnected, + CS.Connecting, + CS.Connected, + CS.ConnectedPendingDisconnect, + CS.Disconnected, + ]); + }); + + test('connection states when a connection misses a keepalive, and then recovers', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + + expect(observedStates).toEqual([ + 'Disconnected', + 'Connecting', + 'Connected', + 'ConnectedPendingKeepAlive', + 'Connected', + ]); + }); + + test('lots of keep alive messages dont add more connection state events', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + + expect(observedStates).toEqual([ + 'Disconnected', + 'Connecting', + 'Connected', + ]); + }); + + test('missed keep alives during a network outage dont add an additional state change', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + reachabilityObserver?.next?.({ online: false }); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); + + expect(observedStates).toEqual([ + 'Disconnected', + 'Connecting', + 'Connected', + 'ConnectedPendingNetwork', + ]); + }); + + test('when the network recovers, keep alives become the concern until one is seen', () => { + monitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); + monitor.record(CONNECTION_CHANGE.CONNECTION_ESTABLISHED); + reachabilityObserver?.next?.({ online: false }); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); + reachabilityObserver?.next?.({ online: true }); + monitor.record(CONNECTION_CHANGE.KEEP_ALIVE); + + expect(observedStates).toEqual([ + 'Disconnected', + 'Connecting', + 'Connected', + 'ConnectedPendingNetwork', + 'ConnectedPendingKeepAlive', + 'Connected', + ]); + }); + }); + + describe('when the network is disconnected', () => { + beforeEach(() => { + reachabilityObserver?.next?.({ online: false }); + + observedStates = []; + subscription?.unsubscribe(); + monitor = new ConnectionStateMonitor(); + subscription = monitor.connectionStateObservable.subscribe(value => { + observedStates.push(value); + }); + }); + + test('starts out disconnected', () => { + expect(observedStates).toEqual(['Disconnected']); + }); + }); +}); diff --git a/packages/pubsub/__tests__/PubSub-unit-test.ts b/packages/pubsub/__tests__/PubSub-unit-test.ts index 969e42d520b..c24f3a62646 100644 --- a/packages/pubsub/__tests__/PubSub-unit-test.ts +++ b/packages/pubsub/__tests__/PubSub-unit-test.ts @@ -1,3 +1,14 @@ +jest.mock('@aws-amplify/core', () => ({ + __esModule: true, + ...jest.requireActual('@aws-amplify/core'), + browserOrNode() { + return { + isBrowser: true, + isNode: false, + }; + }, +})); + import { PubSubClass as PubSub } from '../src/PubSub'; import { MqttOverWSProvider, @@ -8,9 +19,19 @@ import { // import Amplify from '../../src/'; import { Credentials, + Hub, INTERNAL_AWS_APPSYNC_PUBSUB_PROVIDER, + Logger, + Reachability, } from '@aws-amplify/core'; import * as Paho from 'paho-mqtt'; +import { + ConnectionState, + ConnectionState, + CONNECTION_STATE_CHANGE, +} from '../src'; +import { HubConnectionListener } from './helpers'; +import Observable from 'zen-observable-ts'; const pahoClientMockCache = {}; @@ -295,6 +316,123 @@ describe('PubSub', () => { expect(originalProvider.publish).not.toHaveBeenCalled(); expect(newProvider.publish).toHaveBeenCalled(); }); + + describe('Hub connection state changes', () => { + let hubConnectionListener: HubConnectionListener; + + let reachabilityObserver: ZenObservable.Observer<{ online: boolean }>; + + beforeEach(() => { + // Maintain the Hub connection listener, used to monitor the connection messages sent through Hub + hubConnectionListener?.teardown(); + hubConnectionListener = new HubConnectionListener('pubsub'); + + // Setup a mock of the reachability monitor where the initial value is online. + const spyon = jest + .spyOn(Reachability.prototype, 'networkMonitor') + .mockImplementationOnce( + () => + new Observable(observer => { + reachabilityObserver = observer; + }) + ); + reachabilityObserver?.next?.({ online: true }); + }); + + test('test happy case connect -> disconnect cycle', async () => { + const pubsub = new PubSub(); + + const awsIotProvider = new AWSIoTProvider({ + aws_pubsub_region: 'region', + aws_pubsub_endpoint: 'wss://iot.mymockendpoint.org:443/notrealmqtt', + }); + pubsub.addPluggable(awsIotProvider); + + const sub = pubsub.subscribe('topic', { clientId: '123' }).subscribe({ + error: () => {}, + }); + + await hubConnectionListener.waitUntilConnectionStateIn(['Connected']); + sub.unsubscribe(); + awsIotProvider.onDisconnect({ errorCode: 1, clientId: '123' }); + await hubConnectionListener.waitUntilConnectionStateIn([ + 'Disconnected', + ]); + expect(hubConnectionListener.observedConnectionStates).toEqual([ + 'Disconnected', + 'Connecting', + 'Connected', + 'ConnectedPendingDisconnect', + 'Disconnected', + ]); + }); + + test('test network disconnection and recovery', async () => { + const pubsub = new PubSub(); + + const awsIotProvider = new AWSIoTProvider({ + aws_pubsub_region: 'region', + aws_pubsub_endpoint: 'wss://iot.mymockendpoint.org:443/notrealmqtt', + }); + pubsub.addPluggable(awsIotProvider); + + const sub = pubsub.subscribe('topic', { clientId: '123' }).subscribe({ + error: () => {}, + }); + + await hubConnectionListener.waitUntilConnectionStateIn(['Connected']); + + reachabilityObserver?.next?.({ online: false }); + await hubConnectionListener.waitUntilConnectionStateIn([ + 'ConnectedPendingNetwork', + ]); + + reachabilityObserver?.next?.({ online: true }); + await hubConnectionListener.waitUntilConnectionStateIn(['Connected']); + + expect(hubConnectionListener.observedConnectionStates).toEqual([ + 'Disconnected', + 'Connecting', + 'Connected', + 'ConnectedPendingNetwork', + 'Connected', + ]); + }); + + test('test network disconnection followed by connection disruption', async () => { + const pubsub = new PubSub(); + + const awsIotProvider = new AWSIoTProvider({ + aws_pubsub_region: 'region', + aws_pubsub_endpoint: 'wss://iot.mymockendpoint.org:443/notrealmqtt', + }); + pubsub.addPluggable(awsIotProvider); + + const sub = pubsub.subscribe('topic', { clientId: '123' }).subscribe({ + error: () => {}, + }); + + await hubConnectionListener.waitUntilConnectionStateIn(['Connected']); + + reachabilityObserver?.next?.({ online: false }); + await hubConnectionListener.waitUntilConnectionStateIn([ + 'ConnectedPendingNetwork', + ]); + + awsIotProvider.onDisconnect({ errorCode: 1, clientId: '123' }); + await hubConnectionListener.waitUntilConnectionStateIn([ + 'Disconnected', + ]); + + expect(hubConnectionListener.observedConnectionStates).toEqual([ + 'Disconnected', + 'Connecting', + 'Connected', + 'ConnectedPendingNetwork', + 'Disconnected', + ]); + }); + }); }); describe('MqttOverWSProvider local testing config', () => { diff --git a/packages/pubsub/__tests__/helpers.ts b/packages/pubsub/__tests__/helpers.ts index 8328d953def..54870614329 100644 --- a/packages/pubsub/__tests__/helpers.ts +++ b/packages/pubsub/__tests__/helpers.ts @@ -1,4 +1,7 @@ -import * as constants from '../src/Providers/AWSAppSyncRealTimeProvider/constants'; +import { Hub } from '@aws-amplify/core'; +import Observable from 'zen-observable-ts'; +import { ConnectionState as CS, CONNECTION_STATE_CHANGE } from '../src'; +import * as constants from '../src/Providers/constants'; export function delay(timeout) { return new Promise(resolve => { @@ -8,14 +11,90 @@ export function delay(timeout) { }); } +export class HubConnectionListener { + teardownHubListener: () => void; + observedConnectionStates: CS[] = []; + currentConnectionState: CS; + + private connectionStateObservers: ZenObservable.Observer[] = []; + + constructor(channel: string) { + let closeResolver: (value: PromiseLike) => void; + + this.teardownHubListener = Hub.listen(channel, (data: any) => { + const { payload } = data; + if (payload.event === CONNECTION_STATE_CHANGE) { + const connectionState = payload.data.connectionState as CS; + this.observedConnectionStates.push(connectionState); + this.connectionStateObservers.forEach(observer => { + observer?.next?.(connectionState); + }); + this.currentConnectionState = connectionState; + } + }); + } + + /** + * @returns {Observable} - The observable that emits all ConnectionState updates (past and future) + */ + allConnectionStateObserver() { + return new Observable(observer => { + this.observedConnectionStates.forEach(state => { + observer.next(state); + }); + this.connectionStateObservers.push(observer); + }); + } + + /** + * @returns {Observable} - The observable that emits ConnectionState updates (past and future) + */ + connectionStateObserver() { + return new Observable(observer => { + this.connectionStateObservers.push(observer); + }); + } + + /** + * Tear down the Fake Socket state + */ + teardown() { + this.teardownHubListener(); + this.connectionStateObservers.forEach(observer => { + observer?.complete?.(); + }); + } + + async waitForConnectionState(connectionStates: CS[]) { + return new Promise((res, rej) => { + this.connectionStateObserver().subscribe(value => { + if (connectionStates.includes(String(value) as CS)) { + res(undefined); + } + }); + }); + } + + async waitUntilConnectionStateIn(connectionStates: CS[]) { + return new Promise((res, rej) => { + if (connectionStates.includes(this.currentConnectionState)) { + res(undefined); + } + res(this.waitForConnectionState(connectionStates)); + }); + } +} + export class FakeWebSocketInterface { readonly webSocket: FakeWebSocket; - readyForUse: Promise; + readyForUse: Promise; hasClosed: Promise; + hubConnectionListener: HubConnectionListener; private readyResolve: (value: PromiseLike) => void; constructor() { + this.hubConnectionListener = new HubConnectionListener('api'); this.readyForUse = new Promise((res, rej) => { this.readyResolve = res; }); @@ -23,69 +102,108 @@ export class FakeWebSocketInterface { this.hasClosed = new Promise((res, rej) => { closeResolver = res; }); - this.webSocket = new FakeWebSocket(closeResolver); + this.webSocket = new FakeWebSocket(() => closeResolver); + } + + get observedConnectionStates() { + return this.hubConnectionListener.observedConnectionStates; + } + + allConnectionStateObserver() { + return this.hubConnectionListener.allConnectionStateObserver(); + } + + connectionStateObserver() { + return this.hubConnectionListener.connectionStateObserver(); + } + + teardown() { + this.hubConnectionListener.teardown(); } + /** + * Once ready for use, send onOpen and the connection_ack + */ async standardConnectionHandshake() { await this.readyForUse; await this.triggerOpen(); await this.handShakeMessage(); } + /** + * After an open is triggered, the provider has logic that must execute + * which changes the function resolvers assigned to the websocket + */ async triggerOpen() { - // After an open is triggered, the provider has logic that must execute - // which changes the function resolvers assigned to the websocket await this.runAndResolve(() => { this.webSocket.onopen(new Event('', {})); }); } + /** + * After a close is triggered, the provider has logic that must execute + * which changes the function resolvers assigned to the websocket + */ async triggerClose() { - // After a close is triggered, the provider has logic that must execute - // which changes the function resolvers assigned to the websocket await this.runAndResolve(() => { - if (this.webSocket.onclose) - this.webSocket.onclose(new CloseEvent('', {})); + if (this.webSocket.onclose) { + try { + this.webSocket.onclose(new CloseEvent('', {})); + } catch {} + } }); } + /** + * Close the interface and wait until the connection is either disconnected or disrupted + */ async closeInterface() { await this.triggerClose(); // Wait for either hasClosed or a half second has passed - await new Promise(res => { + await new Promise(async res => { // The interface is closed when the socket "hasClosed" this.hasClosed.then(() => res(undefined)); - - // The provider can get pretty wrapped around itself, - // but its safe to continue after half a second, even if it hasn't closed the socket - delay(500).then(() => res(undefined)); + await this.waitUntilConnectionStateIn([CS.Disconnected]); + res(undefined); }); } + /** + * After an error is triggered, the provider has logic that must execute + * which changes the function resolvers assigned to the websocket + */ async triggerError() { - // After an error is triggered, the provider has logic that must execute - // which changes the function resolvers assigned to the websocket await this.runAndResolve(() => { this.webSocket.onerror(new Event('TestError', {})); }); } + /** + * Produce a websocket with a short delay to mimic reality + * @returns A websocket + */ newWebSocket() { - setTimeout(() => this.readyResolve(undefined), 10); + setTimeout(() => this.readyResolve(Promise.resolve()), 10); return this.webSocket; } + /** + * Send a connection_ack + */ async handShakeMessage() { await this.sendMessage( new MessageEvent('connection_ack', { data: JSON.stringify({ type: constants.MESSAGE_TYPES.GQL_CONNECTION_ACK, - payload: { keepAliveTimeout: 100_000 }, + payload: { connectionTimeoutMs: 100_000 }, }), }) ); } + /** + * Send a data message + */ async sendDataMessage(data: {}) { await this.sendMessage( new MessageEvent('data', { @@ -97,6 +215,9 @@ export class FakeWebSocketInterface { ); } + /** + * Emit a message on the socket + */ async sendMessage(message: MessageEvent) { // After a message is sent, it takes a few ms for it to enact provider behavior await this.runAndResolve(() => { @@ -104,15 +225,47 @@ export class FakeWebSocketInterface { }); } + /** + * Run a gicommand and resolve to allow internal behavior to execute + */ async runAndResolve(fn) { fn(); await Promise.resolve(); } + + /** + * DELETE THIS? + */ + async observesConnectionState(connectionState: CS) { + return new Promise((res, rej) => { + this.allConnectionStateObserver().subscribe(value => { + if (value === connectionState) { + res(undefined); + } + }); + }); + } + + /** + * @returns a Promise that will wait for one of the provided states to be observed + */ + async waitForConnectionState(connectionStates: CS[]) { + return this.hubConnectionListener.waitForConnectionState(connectionStates); + } + + /** + * @returns a Promise that will wait until the current state is one of the provided states + */ + async waitUntilConnectionStateIn(connectionStates: CS[]) { + return this.hubConnectionListener.waitUntilConnectionStateIn( + connectionStates + ); + } } class FakeWebSocket implements WebSocket { subscriptionId: string | undefined; - closeResolver?: (value: PromiseLike) => void; + closeResolverFcn: () => (value: PromiseLike) => void; binaryType: BinaryType; bufferedAmount: number; @@ -125,7 +278,8 @@ class FakeWebSocket implements WebSocket { readyState: number; url: string; close(code?: number, reason?: string): void { - if (this.closeResolver) this.closeResolver(undefined); + const closeResolver = this.closeResolverFcn(); + if (closeResolver) closeResolver(Promise.resolve(undefined)); } send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { const parsedInput = JSON.parse(String(data)); @@ -170,8 +324,8 @@ class FakeWebSocket implements WebSocket { throw new Error('Method not implemented dispatchEvent.'); } - constructor(closeResolver?: (value: PromiseLike) => void) { - this.closeResolver = closeResolver; + constructor(closeResolver: () => (value: PromiseLike) => void) { + this.closeResolverFcn = closeResolver; } } diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 1b3b79c50ca..ab054939244 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/pubsub", - "version": "4.3.2", + "version": "4.5.1", "description": "Pubsub category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -46,9 +46,9 @@ "cpx": "^1.5.0" }, "dependencies": { - "@aws-amplify/auth": "4.5.2", - "@aws-amplify/cache": "4.0.40", - "@aws-amplify/core": "4.5.2", + "@aws-amplify/auth": "4.6.4", + "@aws-amplify/cache": "4.0.53", + "@aws-amplify/core": "4.7.2", "graphql": "15.8.0", "paho-mqtt": "^1.1.0", "uuid": "^3.2.1", diff --git a/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/index.ts b/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/index.ts index 6ff83c7bc2a..a56043b5301 100644 --- a/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/index.ts +++ b/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/index.ts @@ -30,19 +30,25 @@ import { import Cache from '@aws-amplify/cache'; import Auth, { GRAPHQL_AUTH_MODE } from '@aws-amplify/auth'; import { AbstractPubSubProvider } from '../PubSubProvider'; -import { CONTROL_MSG } from '../../index'; +import { CONNECTION_STATE_CHANGE, CONTROL_MSG } from '../../index'; + import { AMPLIFY_SYMBOL, AWS_APPSYNC_REALTIME_HEADERS, CONNECTION_INIT_TIMEOUT, DEFAULT_KEEP_ALIVE_TIMEOUT, + DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT, MAX_DELAY_MS, MESSAGE_TYPES, NON_RETRYABLE_CODES, SOCKET_STATUS, START_ACK_TIMEOUT, SUBSCRIPTION_STATUS, -} from './constants'; +} from '../constants'; +import { + ConnectionStateMonitor, + CONNECTION_CHANGE, +} from '../../utils/ConnectionStateMonitor'; const logger = new Logger('AWSAppSyncRealTimeProvider'); @@ -89,8 +95,27 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { private socketStatus: SOCKET_STATUS = SOCKET_STATUS.CLOSED; private keepAliveTimeoutId?: ReturnType; private keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; + private keepAliveAlertTimeoutId?: ReturnType; private subscriptionObserverMap: Map = new Map(); private promiseArray: Array<{ res: Function; rej: Function }> = []; + private readonly connectionStateMonitor = new ConnectionStateMonitor(); + + constructor(options: ProviderOptions = {}) { + super(options); + // Monitor the connection state and pass changes along to Hub + this.connectionStateMonitor.connectionStateObservable.subscribe( + ConnectionState => { + dispatchApiEvent( + CONNECTION_STATE_CHANGE, + { + provider: this, + connectionState: ConnectionState, + }, + `Connection state is ${ConnectionState}` + ); + } + ); + } getNewWebSocket(url, protocol) { return new WebSocket(url, protocol); @@ -147,6 +172,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { }, ], }); + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); observer.complete(); }); @@ -252,6 +278,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { const stringToAWSRealTime = JSON.stringify(subscriptionMessage); try { + this.connectionStateMonitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); await this._initializeWebSocketConnection({ apiKey, appSyncGraphqlEndpoint, @@ -262,6 +289,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { } catch (err) { logger.debug({ err }); const message = err['message'] ?? ''; + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); observer.error({ errors: [ { @@ -366,12 +394,20 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { this.socketStatus = SOCKET_STATUS.CLOSED; return; } + + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSING_CONNECTION); + if (this.awsRealTimeSocket.bufferedAmount > 0) { // Still data on the WebSocket setTimeout(this._closeSocketIfRequired.bind(this), 1000); } else { logger.debug('closing WebSocket...'); - if (this.keepAliveTimeoutId) clearTimeout(this.keepAliveTimeoutId); + if (this.keepAliveTimeoutId) { + clearTimeout(this.keepAliveTimeoutId); + } + if (this.keepAliveAlertTimeoutId) { + clearTimeout(this.keepAliveAlertTimeoutId); + } const tempSocket = this.awsRealTimeSocket; // Cleaning callbacks to avoid race condition, socket still exists tempSocket.onclose = null; @@ -379,6 +415,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { tempSocket.close(1000); this.awsRealTimeSocket = undefined; this.socketStatus = SOCKET_STATUS.CLOSED; + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); } } @@ -432,17 +469,25 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { subscriptionFailedCallback, }); } + this.connectionStateMonitor.record( + CONNECTION_CHANGE.CONNECTION_ESTABLISHED + ); - // TODO: emit event on hub but it requires to store the id first return; } if (type === MESSAGE_TYPES.GQL_CONNECTION_KEEP_ALIVE) { if (this.keepAliveTimeoutId) clearTimeout(this.keepAliveTimeoutId); + if (this.keepAliveAlertTimeoutId) + clearTimeout(this.keepAliveAlertTimeoutId); this.keepAliveTimeoutId = setTimeout( - this._errorDisconnect.bind(this, CONTROL_MSG.TIMEOUT_DISCONNECT), + () => this._errorDisconnect(CONTROL_MSG.TIMEOUT_DISCONNECT), this.keepAliveTimeout ); + this.keepAliveAlertTimeoutId = setTimeout(() => { + this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED); + }, DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT); + this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE); return; } @@ -489,6 +534,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { }); this.subscriptionObserverMap.clear(); if (this.awsRealTimeSocket) { + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); this.awsRealTimeSocket.close(); } @@ -630,6 +676,9 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { logger.debug(`WebSocket connection error`); }; newSocket.onclose = () => { + this.connectionStateMonitor.record( + CONNECTION_CHANGE.CONNECTION_FAILED + ); rej(new Error('Connection handshake error')); }; newSocket.onopen = () => { @@ -698,17 +747,20 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { }; this.awsRealTimeSocket.send(JSON.stringify(gqlInit)); - setTimeout(checkAckOk.bind(this, ackOk), CONNECTION_INIT_TIMEOUT); - } + const checkAckOk = (ackOk: boolean) => { + if (!ackOk) { + this.connectionStateMonitor.record( + CONNECTION_CHANGE.CONNECTION_FAILED + ); + rej( + new Error( + `Connection timeout: ack from AWSAppSyncRealTime was not received after ${CONNECTION_INIT_TIMEOUT} ms` + ) + ); + } + }; - function checkAckOk(ackOk: boolean) { - if (!ackOk) { - rej( - new Error( - `Connection timeout: ack from AWSRealTime was not received on ${CONNECTION_INIT_TIMEOUT} ms` - ) - ); - } + setTimeout(() => checkAckOk(ackOk), CONNECTION_INIT_TIMEOUT); } }); })(); diff --git a/packages/pubsub/src/Providers/MqttOverWSProvider.ts b/packages/pubsub/src/Providers/MqttOverWSProvider.ts index 4b92252e0f4..0a63cc1a9c0 100644 --- a/packages/pubsub/src/Providers/MqttOverWSProvider.ts +++ b/packages/pubsub/src/Providers/MqttOverWSProvider.ts @@ -16,7 +16,13 @@ import Observable from 'zen-observable-ts'; import { AbstractPubSubProvider } from './PubSubProvider'; import { ProviderOptions, SubscriptionObserver } from '../types'; -import { ConsoleLogger as Logger } from '@aws-amplify/core'; +import { ConsoleLogger as Logger, Hub } from '@aws-amplify/core'; +import { + ConnectionStateMonitor, + CONNECTION_CHANGE, +} from '../utils/ConnectionStateMonitor'; +import { AMPLIFY_SYMBOL } from './constants'; +import { CONNECTION_STATE_CHANGE } from '..'; const logger = new Logger('MqttOverWSProvider'); @@ -72,13 +78,32 @@ class ClientsQueue { } } +const dispatchPubSubEvent = (event: string, data: any, message: string) => { + Hub.dispatch('pubsub', { event, data, message }, 'PubSub', AMPLIFY_SYMBOL); +}; + const topicSymbol = typeof Symbol !== 'undefined' ? Symbol('topic') : '@@topic'; export class MqttOverWSProvider extends AbstractPubSubProvider { private _clientsQueue = new ClientsQueue(); + private readonly connectionStateMonitor = new ConnectionStateMonitor(); constructor(options: MqttProviderOptions = {}) { super({ ...options, clientId: options.clientId || uuid() }); + + // Monitor the connection health state and pass changes along to Hub + this.connectionStateMonitor.connectionStateObservable.subscribe( + connectionStateChange => { + dispatchPubSubEvent( + CONNECTION_STATE_CHANGE, + { + provider: this, + connectionState: connectionStateChange, + }, + `Connection state is ${connectionStateChange}` + ); + } + ); } protected get clientId() { @@ -149,6 +174,7 @@ export class MqttOverWSProvider extends AbstractPubSubProvider { public async newClient({ url, clientId }: MqttProviderOptions): Promise { logger.debug('Creating new MQTT client', clientId); + this.connectionStateMonitor.record(CONNECTION_CHANGE.OPENING_CONNECTION); // @ts-ignore const client = new Paho.Client(url, clientId); // client.trace = (args) => logger.debug(clientId, JSON.stringify(args, null, 2)); @@ -168,6 +194,7 @@ export class MqttOverWSProvider extends AbstractPubSubProvider { errorCode: number; }) => { this.onDisconnect({ clientId, errorCode, ...args }); + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); }; await new Promise((resolve, reject) => { @@ -175,10 +202,19 @@ export class MqttOverWSProvider extends AbstractPubSubProvider { useSSL: this.isSSLEnabled, mqttVersion: 3, onSuccess: () => resolve(client), - onFailure: reject, + onFailure: () => { + reject(); + this.connectionStateMonitor.record( + CONNECTION_CHANGE.CONNECTION_FAILED + ); + }, }); }); + this.connectionStateMonitor.record( + CONNECTION_CHANGE.CONNECTION_ESTABLISHED + ); + return client; } @@ -196,6 +232,7 @@ export class MqttOverWSProvider extends AbstractPubSubProvider { if (client && client.isConnected()) { client.disconnect(); + this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED); } this.clientsQueue.remove(clientId); } @@ -293,6 +330,10 @@ export class MqttOverWSProvider extends AbstractPubSubProvider { this._clientIdObservers.get(clientId)?.delete(observer); // No more observers per client => client not needed anymore if (this._clientIdObservers.get(clientId)?.size === 0) { + this.connectionStateMonitor.record( + CONNECTION_CHANGE.CLOSING_CONNECTION + ); + this.disconnect(clientId); this._clientIdObservers.delete(clientId); } diff --git a/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/constants.ts b/packages/pubsub/src/Providers/constants.ts similarity index 94% rename from packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/constants.ts rename to packages/pubsub/src/Providers/constants.ts index a529573187e..b8e5c04cdb5 100644 --- a/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider/constants.ts +++ b/packages/pubsub/src/Providers/constants.ts @@ -93,3 +93,8 @@ export const START_ACK_TIMEOUT = 15000; * Default Time in milleseconds to wait for GQL_CONNECTION_KEEP_ALIVE message */ export const DEFAULT_KEEP_ALIVE_TIMEOUT = 5 * 60 * 1000; + +/** + * Default Time in milleseconds to alert for missed GQL_CONNECTION_KEEP_ALIVE message + */ +export const DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT = 65 * 1000; diff --git a/packages/pubsub/src/index.ts b/packages/pubsub/src/index.ts index d44e2a1f28a..cf84732d317 100644 --- a/packages/pubsub/src/index.ts +++ b/packages/pubsub/src/index.ts @@ -22,6 +22,9 @@ enum CONTROL_MSG { TIMEOUT_DISCONNECT = 'Timeout disconnect', } +export const CONNECTION_STATE_CHANGE = 'ConnectionStateChange'; +export { ConnectionState } from './types'; + export { PubSub, CONTROL_MSG }; /** diff --git a/packages/pubsub/src/types/index.ts b/packages/pubsub/src/types/index.ts index 99cfd92756d..bd6450c9a58 100644 --- a/packages/pubsub/src/types/index.ts +++ b/packages/pubsub/src/types/index.ts @@ -19,3 +19,39 @@ export interface SubscriptionObserver { error(errorValue: any): void; complete(): void; } + +/** @enum {string} */ +export enum ConnectionState { + /* + * The connection is alive and healthy + */ + Connected = 'Connected', + /* + * The connection is alive, but the connection is offline + */ + ConnectedPendingNetwork = 'ConnectedPendingNetwork', + /* + * The connection has been disconnected while in use + */ + ConnectionDisrupted = 'ConnectionDisrupted', + /* + * The connection has been disconnected and the network is offline + */ + ConnectionDisruptedPendingNetwork = 'ConnectionDisruptedPendingNetwork', + /* + * The connection is in the process of connecting + */ + Connecting = 'Connecting', + /* + * The connection is not in use and is being disconnected + */ + ConnectedPendingDisconnect = 'ConnectedPendingDisconnect', + /* + * The connection is not in use and has been disconnected + */ + Disconnected = 'Disconnected', + /* + * The connection is alive, but a keep alive message has been missed + */ + ConnectedPendingKeepAlive = 'ConnectedPendingKeepAlive', +} diff --git a/packages/pubsub/src/utils/ConnectionStateMonitor.ts b/packages/pubsub/src/utils/ConnectionStateMonitor.ts new file mode 100644 index 00000000000..db91d93760c --- /dev/null +++ b/packages/pubsub/src/utils/ConnectionStateMonitor.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2017-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Reachability } from '@aws-amplify/core'; +import Observable, { ZenObservable } from 'zen-observable-ts'; +import { ConnectionState } from '../index'; +import { ReachabilityMonitor } from './ReachabilityMonitor'; + +// Internal types for tracking different connection states +type LinkedConnectionState = 'connected' | 'disconnected'; +type LinkedHealthState = 'healthy' | 'unhealthy'; +type LinkedConnectionStates = { + networkState: LinkedConnectionState; + connectionState: LinkedConnectionState | 'connecting'; + intendedConnectionState: LinkedConnectionState; + keepAliveState: LinkedHealthState; +}; + +export const CONNECTION_CHANGE: { + [key in + | 'KEEP_ALIVE_MISSED' + | 'KEEP_ALIVE' + | 'CONNECTION_ESTABLISHED' + | 'CONNECTION_FAILED' + | 'CLOSING_CONNECTION' + | 'OPENING_CONNECTION' + | 'CLOSED' + | 'ONLINE' + | 'OFFLINE']: Partial; +} = { + KEEP_ALIVE_MISSED: { keepAliveState: 'unhealthy' }, + KEEP_ALIVE: { keepAliveState: 'healthy' }, + CONNECTION_ESTABLISHED: { connectionState: 'connected' }, + CONNECTION_FAILED: { + intendedConnectionState: 'disconnected', + connectionState: 'disconnected', + }, + CLOSING_CONNECTION: { intendedConnectionState: 'disconnected' }, + OPENING_CONNECTION: { + intendedConnectionState: 'connected', + connectionState: 'connecting', + }, + CLOSED: { connectionState: 'disconnected' }, + ONLINE: { networkState: 'connected' }, + OFFLINE: { networkState: 'disconnected' }, +}; + +export class ConnectionStateMonitor { + /** + * @private + */ + private _linkedConnectionState: LinkedConnectionStates; + private _linkedConnectionStateObservable: Observable; + private _linkedConnectionStateObserver: ZenObservable.SubscriptionObserver; + private _networkMonitoringSubscription?: ZenObservable.Subscription; + + constructor() { + this._networkMonitoringSubscription = undefined; + this._linkedConnectionState = { + networkState: 'connected', + connectionState: 'disconnected', + intendedConnectionState: 'disconnected', + keepAliveState: 'healthy', + }; + + this._linkedConnectionStateObservable = + new Observable(connectionStateObserver => { + connectionStateObserver.next(this._linkedConnectionState); + this._linkedConnectionStateObserver = connectionStateObserver; + }); + } + + /** + * Turn network state monitoring on if it isn't on already + */ + private enableNetworkMonitoring() { + // Maintain the network state based on the reachability monitor + if (this._networkMonitoringSubscription === undefined) { + this._networkMonitoringSubscription = ReachabilityMonitor().subscribe( + ({ online }) => { + this.record( + online ? CONNECTION_CHANGE.ONLINE : CONNECTION_CHANGE.OFFLINE + ); + } + ); + } + } + + /** + * Turn network state monitoring off if it isn't off already + */ + private disableNetworkMonitoring() { + this._networkMonitoringSubscription?.unsubscribe(); + this._networkMonitoringSubscription = undefined; + } + + /** + * Get the observable that allows us to monitor the connection state + * + * @returns {Observable} - The observable that emits ConnectionState updates + */ + public get connectionStateObservable(): Observable { + let previous: ConnectionState; + + // The linked state aggregates state changes to any of the network, connection, + // intendedConnection and keepAliveHealth. Some states will change these independent + // states without changing the overall connection state. + + // After translating from linked states to ConnectionState, then remove any duplicates + return this._linkedConnectionStateObservable + .map(value => { + return this.connectionStatesTranslator(value); + }) + .filter(current => { + const toInclude = current !== previous; + previous = current; + return toInclude; + }); + } + + /* + * Updates local connection state and emits the full state to the observer. + */ + record(statusUpdates: Partial) { + // Maintain the network monitor + if (statusUpdates.intendedConnectionState === 'connected') { + this.enableNetworkMonitoring(); + } else if (statusUpdates.intendedConnectionState === 'disconnected') { + this.disableNetworkMonitoring(); + } + + // Maintain the socket state + const newSocketStatus = { + ...this._linkedConnectionState, + ...statusUpdates, + }; + + this._linkedConnectionState = { ...newSocketStatus }; + + this._linkedConnectionStateObserver.next(this._linkedConnectionState); + } + + /* + * Translate the ConnectionState structure into a specific ConnectionState string literal union + */ + private connectionStatesTranslator({ + connectionState, + networkState, + intendedConnectionState, + keepAliveState, + }: LinkedConnectionStates): ConnectionState { + if (connectionState === 'connected' && networkState === 'disconnected') + return ConnectionState.ConnectedPendingNetwork; + + if ( + connectionState === 'connected' && + intendedConnectionState === 'disconnected' + ) + return ConnectionState.ConnectedPendingDisconnect; + + if ( + connectionState === 'disconnected' && + intendedConnectionState === 'connected' && + networkState === 'disconnected' + ) + return ConnectionState.ConnectionDisruptedPendingNetwork; + + if ( + connectionState === 'disconnected' && + intendedConnectionState === 'connected' + ) + return ConnectionState.ConnectionDisrupted; + + if (connectionState === 'connected' && keepAliveState === 'unhealthy') + return ConnectionState.ConnectedPendingKeepAlive; + + // All remaining states directly correspond to the connection state + if (connectionState === 'connecting') return ConnectionState.Connecting; + if (connectionState === 'disconnected') return ConnectionState.Disconnected; + return ConnectionState.Connected; + } +} diff --git a/packages/pubsub/src/utils/ReachabilityMonitor/index.native.ts b/packages/pubsub/src/utils/ReachabilityMonitor/index.native.ts new file mode 100644 index 00000000000..e3ca875a602 --- /dev/null +++ b/packages/pubsub/src/utils/ReachabilityMonitor/index.native.ts @@ -0,0 +1,5 @@ +import { Reachability } from '@aws-amplify/core'; +import { default as NetInfo } from '@react-native-community/netinfo'; + +export const ReachabilityMonitor = () => + new Reachability().networkMonitor(NetInfo); diff --git a/packages/pubsub/src/utils/ReachabilityMonitor/index.ts b/packages/pubsub/src/utils/ReachabilityMonitor/index.ts new file mode 100644 index 00000000000..c33ae7def12 --- /dev/null +++ b/packages/pubsub/src/utils/ReachabilityMonitor/index.ts @@ -0,0 +1,3 @@ +import { Reachability } from '@aws-amplify/core'; + +export const ReachabilityMonitor = () => new Reachability().networkMonitor(); diff --git a/packages/pushnotification/CHANGELOG.md b/packages/pushnotification/CHANGELOG.md index 45e2b6f6c66..4a7f9632d19 100644 --- a/packages/pushnotification/CHANGELOG.md +++ b/packages/pushnotification/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.3.30](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.29...@aws-amplify/pushnotification@4.3.30) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.29](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.28...@aws-amplify/pushnotification@4.3.29) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.28](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.27...@aws-amplify/pushnotification@4.3.28) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.27](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.26...@aws-amplify/pushnotification@4.3.27) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.26](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.25...@aws-amplify/pushnotification@4.3.26) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.25](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.24...@aws-amplify/pushnotification@4.3.25) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.24](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.23...@aws-amplify/pushnotification@4.3.24) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.23](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.22...@aws-amplify/pushnotification@4.3.23) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.22](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.21...@aws-amplify/pushnotification@4.3.22) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.21](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.20...@aws-amplify/pushnotification@4.3.21) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.20](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.19...@aws-amplify/pushnotification@4.3.20) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.19](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.18...@aws-amplify/pushnotification@4.3.19) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + +## [4.3.18](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.17...@aws-amplify/pushnotification@4.3.18) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/pushnotification + + + + + ## [4.3.17](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/pushnotification@4.3.16...@aws-amplify/pushnotification@4.3.17) (2022-04-14) diff --git a/packages/pushnotification/package.json b/packages/pushnotification/package.json index a18261c3164..6908104c857 100644 --- a/packages/pushnotification/package.json +++ b/packages/pushnotification/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/pushnotification", - "version": "4.3.17", + "version": "4.3.30", "description": "Push notifications category of aws-amplify", "main": "./lib/index.js", "module": "./lib/index.js", @@ -46,7 +46,7 @@ "webpack": "^3.5.5" }, "dependencies": { - "@aws-amplify/core": "4.5.2", + "@aws-amplify/core": "4.7.2", "@react-native-community/push-notification-ios": "1.0.3" }, "jest": { diff --git a/packages/storage/CHANGELOG.md b/packages/storage/CHANGELOG.md index f95676aadaf..83d858f51c6 100644 --- a/packages/storage/CHANGELOG.md +++ b/packages/storage/CHANGELOG.md @@ -3,6 +3,120 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.5.3...@aws-amplify/storage@4.5.4) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.5.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.5.2...@aws-amplify/storage@4.5.3) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.5.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.5.1...@aws-amplify/storage@4.5.2) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.5.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.5.0...@aws-amplify/storage@4.5.1) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +# [4.5.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.29...@aws-amplify/storage@4.5.0) (2022-07-28) + + +### Features + +* **@aws-amplify/storage:** Access all files from S3 with List API ([#10095](https://github.com/aws-amplify/amplify-js/issues/10095)) ([366c32e](https://github.com/aws-amplify/amplify-js/commit/366c32e2d87d73210bbd01ca1da55a5899f5a503)) + + + + + +## [4.4.29](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.28...@aws-amplify/storage@4.4.29) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.4.28](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.27...@aws-amplify/storage@4.4.28) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.4.27](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.26...@aws-amplify/storage@4.4.27) (2022-06-18) + + +### Bug Fixes + +* remove comments ([b5c6825](https://github.com/aws-amplify/amplify-js/commit/b5c6825a28e58986b26cce662f8db7a3623146e7)) +* update axios ([67316d7](https://github.com/aws-amplify/amplify-js/commit/67316d78fd829b9d4875a25d00719b175738e594)) + + + + + +## [4.4.26](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.25...@aws-amplify/storage@4.4.26) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.4.25](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.24...@aws-amplify/storage@4.4.25) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.4.24](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.23...@aws-amplify/storage@4.4.24) (2022-05-23) + + +### Bug Fixes + +* **@aws-amplify/storage:** throw error if all upload parts complete but upload cannot be finished ([#9317](https://github.com/aws-amplify/amplify-js/issues/9317)) ([798a8f0](https://github.com/aws-amplify/amplify-js/commit/798a8f075021582cc3e1f4f8ad239562ec4de566)) + + + + + +## [4.4.23](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.22...@aws-amplify/storage@4.4.23) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + +## [4.4.22](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.21...@aws-amplify/storage@4.4.22) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/storage + + + + + ## [4.4.21](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/storage@4.4.20...@aws-amplify/storage@4.4.21) (2022-04-14) **Note:** Version bump only for package @aws-amplify/storage diff --git a/packages/storage/__tests__/providers/AWSS3Provider-unit-test.ts b/packages/storage/__tests__/providers/AWSS3Provider-unit-test.ts index ae502413b39..9a004e3c438 100644 --- a/packages/storage/__tests__/providers/AWSS3Provider-unit-test.ts +++ b/packages/storage/__tests__/providers/AWSS3Provider-unit-test.ts @@ -15,7 +15,7 @@ import { Logger, Hub, Credentials, ICredentials } from '@aws-amplify/core'; import * as formatURL from '@aws-sdk/util-format-url'; import { S3Client, - ListObjectsCommand, + ListObjectsV2Command, CreateMultipartUploadCommand, UploadPartCommand, } from '@aws-sdk/client-s3'; @@ -40,26 +40,25 @@ const mockEventEmitter = { removeAllListeners: mockRemoveAllListeners, }; -jest.mock('events', function() { +jest.mock('events', function () { return { EventEmitter: jest.fn().mockImplementation(() => mockEventEmitter), }; }); S3Client.prototype.send = jest.fn(async command => { - if (command instanceof ListObjectsCommand) { + if (command instanceof ListObjectsV2Command) { + const resultObj = { + Key: 'public/path/itemsKey', + ETag: 'etag', + LastModified: 'lastmodified', + Size: 'size', + }; if (command.input.Prefix === 'public/emptyListResultsPath') { return {}; } return { - Contents: [ - { - Key: 'public/path/itemsKey', - ETag: 'etag', - LastModified: 'lastmodified', - Size: 'size', - }, - ], + Contents: [resultObj], }; } return 'data'; @@ -137,7 +136,8 @@ describe('StorageProvider test', () => { const aws_options = { aws_user_files_s3_bucket: 'bucket', aws_user_files_s3_bucket_region: 'region', - aws_user_files_s3_dangerously_connect_to_http_endpoint_for_testing: true, + aws_user_files_s3_dangerously_connect_to_http_endpoint_for_testing: + true, }; const config = storage.configure(aws_options); @@ -311,7 +311,8 @@ describe('StorageProvider test', () => { }); await storage.get('key', { download: true, - progressCallback: ('this is not a function' as unknown) as S3ProviderGetConfig['progressCallback'], // this is intentional + progressCallback: + 'this is not a function' as unknown as S3ProviderGetConfig['progressCallback'], // this is intentional }); expect(loggerSpy).toHaveBeenCalledWith( 'WARN', @@ -729,7 +730,8 @@ describe('StorageProvider test', () => { const storage = new StorageProvider(); storage.configure(options); await storage.put('key', 'object', { - progressCallback: ('hello' as unknown) as S3ProviderGetConfig['progressCallback'], // this is intentional + progressCallback: + 'hello' as unknown as S3ProviderGetConfig['progressCallback'], // this is intentional }); expect(loggerSpy).toHaveBeenCalledWith( 'WARN', @@ -947,6 +949,12 @@ describe('StorageProvider test', () => { }); describe('list test', () => { + const resultObj = { + eTag: 'etag', + key: 'path/itemsKey', + lastModified: 'lastmodified', + size: 'size', + }; test('list object successfully', async () => { jest.spyOn(Credentials, 'get').mockImplementationOnce(() => { return new Promise((res, rej) => { @@ -960,15 +968,11 @@ describe('StorageProvider test', () => { expect.assertions(2); expect(await storage.list('path', { level: 'public' })).toEqual([ - { - eTag: 'etag', - key: 'path/itemsKey', - lastModified: 'lastmodified', - size: 'size', - }, + resultObj, ]); expect(spyon.mock.calls[0][0].input).toEqual({ Bucket: 'bucket', + MaxKeys: 1000, Prefix: 'public/path', }); spyon.mockClear(); @@ -991,6 +995,7 @@ describe('StorageProvider test', () => { ).toEqual([]); expect(spyon.mock.calls[0][0].input).toEqual({ Bucket: 'bucket', + MaxKeys: 1000, Prefix: 'public/emptyListResultsPath', }); spyon.mockClear(); @@ -1011,16 +1016,10 @@ describe('StorageProvider test', () => { expect.assertions(3); expect( await storage.list('path', { level: 'public', track: true }) - ).toEqual([ - { - eTag: 'etag', - key: 'path/itemsKey', - lastModified: 'lastmodified', - size: 'size', - }, - ]); + ).toEqual([resultObj]); expect(spyon.mock.calls[0][0].input).toEqual({ Bucket: 'bucket', + MaxKeys: 1000, Prefix: 'public/path', }); expect(spyon2).toBeCalledWith( @@ -1052,14 +1051,7 @@ describe('StorageProvider test', () => { expect.assertions(2); expect( await storage.list('path', { level: 'public', maxKeys: 1 }) - ).toEqual([ - { - eTag: 'etag', - key: 'path/itemsKey', - lastModified: 'lastmodified', - size: 'size', - }, - ]); + ).toEqual([resultObj]); expect(spyon.mock.calls[0][0].input).toEqual({ Bucket: 'bucket', Prefix: 'public/path', @@ -1070,6 +1062,66 @@ describe('StorageProvider test', () => { curCredSpyOn.mockClear(); }); + test('list object with maxKeys with ALL having 3 pages', async () => { + const curCredSpyOn = jest + .spyOn(Credentials, 'get') + .mockImplementationOnce(() => { + return new Promise((res, rej) => { + res({}); + }); + }); + const storage = new StorageProvider(); + storage.configure(options); + const listResultObj = { + Key: 'public/path/itemsKey', + ETag: 'etag', + LastModified: 'lastmodified', + Size: 'size', + }; + let methodCalls = 0; + let continuationToken = 'TEST_TOKEN'; + let listResult = []; + const listAllFunction = async command => { + if (command instanceof ListObjectsV2Command) { + let token = undefined; + methodCalls++; + if (command.input.ContinuationToken === undefined || methodCalls < 3) + token = continuationToken; + + if (command.input.Prefix === 'public/listALLResultsPath') { + return { + Contents: [listResultObj], + NextContinuationToken: token, + }; + } + } + return 'data'; + }; + S3Client.prototype.send = jest.fn(listAllFunction); + const spyon = jest.spyOn(S3Client.prototype, 'send'); + expect.assertions(5); + for (let i = 0; i < 3; i++) listResult.push(resultObj); + expect( + await storage.list('listALLResultsPath', { + level: 'public', + maxKeys: 'ALL', + }) + ).toEqual(listResult); + expect(spyon).toHaveBeenCalledTimes(3); + const inputResult = { + Bucket: 'bucket', + Prefix: 'public/listALLResultsPath', + MaxKeys: 1000, + ContinuationToken: undefined, + }; + for (let i = 0; i < 3; i++) { + expect(spyon.mock.calls[i][0].input).toEqual(inputResult); + inputResult.ContinuationToken = continuationToken; + } + spyon.mockClear(); + curCredSpyOn.mockClear(); + }); + test('list object failed', async () => { jest.spyOn(Credentials, 'get').mockImplementationOnce(() => { return new Promise((res, rej) => { @@ -1158,13 +1210,10 @@ describe('StorageProvider test', () => { // wrong key type await expect( - storage.copy( - ({ level: 'public', key: 123 } as unknown) as S3CopySource, - { - key: 'dest', - level: 'public', - } - ) + storage.copy({ level: 'public', key: 123 } as unknown as S3CopySource, { + key: 'dest', + level: 'public', + }) ).rejects.toThrowError( 'source param should be an object with the property "key" with value of type string' ); @@ -1188,10 +1237,10 @@ describe('StorageProvider test', () => { // wrong key type await expect( - storage.copy({ key: 'src', level: 'public' }, ({ + storage.copy({ key: 'src', level: 'public' }, { key: 123, level: 'public', - } as unknown) as S3CopyDestination) + } as unknown as S3CopyDestination) ).rejects.toThrowError( 'destination param should be an object with the property "key" with value of type string' ); diff --git a/packages/storage/__tests__/providers/AWSS3ProviderManagedUpload-unit-test.ts b/packages/storage/__tests__/providers/AWSS3ProviderManagedUpload-unit-test.ts index 0484ad64547..106fce2e1b9 100644 --- a/packages/storage/__tests__/providers/AWSS3ProviderManagedUpload-unit-test.ts +++ b/packages/storage/__tests__/providers/AWSS3ProviderManagedUpload-unit-test.ts @@ -269,7 +269,7 @@ describe('multi part upload tests', () => { try { await uploader.upload(); } catch (error) { - expect(error.message).toBe('Upload was cancelled.'); + expect(error.message).toBe('Part 2 just going to fail in 100ms'); } // Should have called 5 times => @@ -314,19 +314,10 @@ describe('multi part upload tests', () => { Key: testParams.Key, UploadId: testUploadId, }); - // Progress reporting works as well - expect(eventSpy).toHaveBeenNthCalledWith(1, { - key: testParams.Key, - loaded: testMinPartSize, - part: 1, - total: testParams.Body.length, - }); - expect(eventSpy).toHaveBeenNthCalledWith(2, { - key: testParams.Key, - loaded: testParams.Body.length, - part: 2, - total: testParams.Body.length, - }); + + // As the 'sendUploadProgress' happens when the upload is 100% complete, + // it won't be called, as an error is thrown before upload completion. + expect(eventSpy).toBeCalledTimes(0); }); test('error case: cleanup failed', async () => { @@ -334,7 +325,13 @@ describe('multi part upload tests', () => { if (command instanceof CreateMultipartUploadCommand) { return Promise.resolve({ UploadId: testUploadId }); } else if (command instanceof UploadPartCommand) { - return Promise.reject(new Error('failed to upload')); + return Promise.resolve({ + PartNumber: testParams.part, + Body: testParams.body, + UploadId: testUploadId, + Key: testParams.key, + Bucket: testParams.bucket, + }); } else if (command instanceof ListPartsCommand) { return Promise.resolve({ Parts: [ @@ -353,7 +350,7 @@ describe('multi part upload tests', () => { new events.EventEmitter() ); await expect(uploader.upload()).rejects.toThrow( - 'Upload was cancelled. Multi Part upload clean up failed' + 'Multipart upload clean up failed.' ); }); @@ -366,7 +363,7 @@ describe('multi part upload tests', () => { ETag: 'test_etag_' + command.input.PartNumber, }); } else if (command instanceof CompleteMultipartUploadCommand) { - return Promise.reject('error'); + return Promise.reject(new Error('Error completing multipart upload.')); } }); const loggerSpy = jest.spyOn(Logger.prototype, '_log'); @@ -375,11 +372,10 @@ describe('multi part upload tests', () => { testOpts, new events.EventEmitter() ); - await uploader.upload(); - expect(loggerSpy).toHaveBeenCalledWith( - 'ERROR', - 'error happened while finishing the upload. Cancelling the multipart upload', - 'error' - ); - }); + + await expect(uploader.upload()).rejects.toThrow('Error completing multipart upload.'); + expect(loggerSpy).toHaveBeenNthCalledWith(1, 'DEBUG', 'testUploadId'); + expect(loggerSpy).toHaveBeenNthCalledWith(2, 'ERROR', 'Error happened while finishing the upload.'); + expect(loggerSpy).toHaveBeenNthCalledWith(3, 'ERROR', 'Error. Cancelling the multipart upload.'); + }) }); diff --git a/packages/storage/package.json b/packages/storage/package.json index a7a666e931b..73d88836922 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/storage", - "version": "4.4.21", + "version": "4.5.4", "description": "Storage category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -41,12 +41,12 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/core": "4.5.2", + "@aws-amplify/core": "4.7.2", "@aws-sdk/client-s3": "3.6.1", "@aws-sdk/s3-request-presigner": "3.6.1", "@aws-sdk/util-create-request": "3.6.1", "@aws-sdk/util-format-url": "3.6.1", - "axios": "0.21.4", + "axios": "0.26.0", "events": "^3.1.0" }, "jest": { diff --git a/packages/storage/src/providers/AWSS3Provider.ts b/packages/storage/src/providers/AWSS3Provider.ts index a9d59334eac..53057f436fd 100644 --- a/packages/storage/src/providers/AWSS3Provider.ts +++ b/packages/storage/src/providers/AWSS3Provider.ts @@ -22,13 +22,14 @@ import { S3Client, GetObjectCommand, DeleteObjectCommand, - ListObjectsCommand, + ListObjectsV2Command, GetObjectCommandOutput, DeleteObjectCommandInput, CopyObjectCommandInput, CopyObjectCommand, PutObjectCommandInput, GetObjectCommandInput, + ListObjectsV2Request, } from '@aws-sdk/client-s3'; import { formatUrl } from '@aws-sdk/util-format-url'; import { createRequest } from '@aws-sdk/util-create-request'; @@ -56,6 +57,7 @@ import { S3ProviderPutOutput, ResumableUploadConfig, UploadTask, + S3ClientOptions, } from '../types'; import { StorageErrorStrings } from '../common/StorageErrorStrings'; import { dispatchStorageEvent } from '../common/StorageUtils'; @@ -67,6 +69,7 @@ import { autoAdjustClockskewMiddlewareOptions, createS3Client, } from '../common/S3ClientUtils'; +import { S3ProviderListOutputWithToken } from '.././types/AWSS3Provider'; import { AWSS3ProviderManagedUpload } from './AWSS3ProviderManagedUpload'; import { AWSS3UploadTask, TaskEvents } from './AWSS3UploadTask'; import { UPLOADS_STORAGE_KEY } from '../common/StorageConstants'; @@ -678,6 +681,31 @@ export class AWSS3Provider implements StorageProvider { throw error; } } + private async _list( + params: ListObjectsV2Request, + opt: S3ClientOptions, + prefix: string + ): Promise { + const result: S3ProviderListOutputWithToken = { + contents: [], + nextToken: '', + }; + const s3 = this._createNewS3Client(opt); + const listObjectsV2Command = new ListObjectsV2Command({ ...params }); + const response = await s3.send(listObjectsV2Command); + if (response && response.Contents) { + result.contents = response.Contents.map(item => { + return { + key: item.Key.substr(prefix.length), + eTag: item.ETag, + lastModified: item.LastModified, + size: item.Size, + }; + }); + result.nextToken = response.NextContinuationToken; + } + return result; + } /** * List bucket objects relative to the level and prefix specified @@ -694,34 +722,38 @@ export class AWSS3Provider implements StorageProvider { if (!credentialsOK || !this._isWithCredentials(this._config)) { throw new Error(StorageErrorStrings.NO_CREDENTIALS); } - const opt = Object.assign({}, this._config, config); + const opt: S3ClientOptions = Object.assign({}, this._config, config); const { bucket, track, maxKeys } = opt; - const prefix = this._prefix(opt); const final_path = prefix + path; - const s3 = this._createNewS3Client(opt); logger.debug('list ' + path + ' from ' + final_path); - - const params = { - Bucket: bucket, - Prefix: final_path, - MaxKeys: maxKeys, - }; - - const listObjectsCommand = new ListObjectsCommand(params); - try { - const response = await s3.send(listObjectsCommand); - let list: S3ProviderListOutput = []; - if (response && response.Contents) { - list = response.Contents.map(item => { - return { - key: item.Key.substr(prefix.length), - eTag: item.ETag, - lastModified: item.LastModified, - size: item.Size, - }; - }); + const list: S3ProviderListOutput = []; + let token: string; + let listResult: S3ProviderListOutputWithToken; + const params: ListObjectsV2Request = { + Bucket: bucket, + Prefix: final_path, + MaxKeys: 1000, + }; + if (maxKeys === 'ALL') { + do { + params.ContinuationToken = token; + params.MaxKeys = 1000; + listResult = await this._list(params, opt, prefix); + list.push(...listResult.contents); + if (listResult.nextToken) token = listResult.nextToken; + } while (listResult.nextToken); + } else { + maxKeys < 1000 || typeof maxKeys === 'string' + ? (params.MaxKeys = maxKeys) + : (params.MaxKeys = 1000); + listResult = await this._list(params, opt, prefix); + list.push(...listResult.contents); + if (maxKeys > 1000) + logger.warn( + "maxkeys can be from 0 - 1000 or 'ALL'. To list all files you can set maxKeys to 'ALL'." + ); } dispatchStorageEvent( track, diff --git a/packages/storage/src/providers/AWSS3ProviderManagedUpload.ts b/packages/storage/src/providers/AWSS3ProviderManagedUpload.ts index 705f06da5c1..e8ce5fdb130 100644 --- a/packages/storage/src/providers/AWSS3ProviderManagedUpload.ts +++ b/packages/storage/src/providers/AWSS3ProviderManagedUpload.ts @@ -58,8 +58,8 @@ export class AWSS3ProviderManagedUpload { private params: PutObjectRequest = null; private opts = null; private completedParts: CompletedPart[] = []; - private cancel = false; private s3client: S3Client; + private uploadId = null; // Progress reporting private bytesUploaded = 0; @@ -74,79 +74,87 @@ export class AWSS3ProviderManagedUpload { } public async upload() { - this.body = await this.validateAndSanitizeBody(this.params.Body); - this.totalBytesToUpload = this.byteLength(this.body); - if (this.totalBytesToUpload <= this.minPartSize) { - // Multipart upload is not required. Upload the sanitized body as is - this.params.Body = this.body; - const putObjectCommand = new PutObjectCommand(this.params); - return this.s3client.send(putObjectCommand); - } else { - // Step 1: Initiate the multi part upload - const uploadId = await this.createMultiPartUpload(); - - // Step 2: Upload chunks in parallel as requested - const numberOfPartsToUpload = Math.ceil( - this.totalBytesToUpload / this.minPartSize - ); - - const parts: Part[] = this.createParts(); - for ( - let start = 0; - start < numberOfPartsToUpload; - start += this.queueSize - ) { - /** This first block will try to cancel the upload if the cancel - * request came before any parts uploads have started. - **/ - await this.checkIfUploadCancelled(uploadId); - - // Upload as many as `queueSize` parts simultaneously - await this.uploadParts( - uploadId, - parts.slice(start, start + this.queueSize) + try { + this.body = await this.validateAndSanitizeBody(this.params.Body); + this.totalBytesToUpload = this.byteLength(this.body); + if (this.totalBytesToUpload <= this.minPartSize) { + // Multipart upload is not required. Upload the sanitized body as is + this.params.Body = this.body; + const putObjectCommand = new PutObjectCommand(this.params); + return this.s3client.send(putObjectCommand); + } else { + // Step 1: Initiate the multi part upload + this.uploadId = await this.createMultiPartUpload(); + + // Step 2: Upload chunks in parallel as requested + const numberOfPartsToUpload = Math.ceil( + this.totalBytesToUpload / this.minPartSize ); - /** Call cleanup a second time in case there were part upload requests - * in flight. This is to ensure that all parts are cleaned up. - */ - await this.checkIfUploadCancelled(uploadId); - } + const parts: Part[] = this.createParts(); + for ( + let start = 0; + start < numberOfPartsToUpload; + start += this.queueSize + ) { + + // Upload as many as `queueSize` parts simultaneously + await this.uploadParts( + this.uploadId, + parts.slice(start, start + this.queueSize) + ); + } - parts.map(part => { - this.removeEventListener(part); - }); + parts.map(part => { + this.removeEventListener(part); + }); - // Step 3: Finalize the upload such that S3 can recreate the file - return await this.finishMultiPartUpload(uploadId); + // Step 3: Finalize the upload such that S3 can recreate the file + return await this.finishMultiPartUpload(this.uploadId); + } + } catch (error) { + // if any error is thrown, call cleanup + await this.cleanup(this.uploadId); + logger.error('Error. Cancelling the multipart upload.'); + throw error; } } private createParts(): Part[] { - const parts: Part[] = []; - for (let bodyStart = 0; bodyStart < this.totalBytesToUpload; ) { - const bodyEnd = Math.min( - bodyStart + this.minPartSize, - this.totalBytesToUpload - ); - parts.push({ - bodyPart: this.body.slice(bodyStart, bodyEnd), - partNumber: parts.length + 1, - emitter: new events.EventEmitter(), - _lastUploadedBytes: 0, - }); - bodyStart += this.minPartSize; + try { + const parts: Part[] = []; + for (let bodyStart = 0; bodyStart < this.totalBytesToUpload; ) { + const bodyEnd = Math.min( + bodyStart + this.minPartSize, + this.totalBytesToUpload + ); + parts.push({ + bodyPart: this.body.slice(bodyStart, bodyEnd), + partNumber: parts.length + 1, + emitter: new events.EventEmitter(), + _lastUploadedBytes: 0, + }); + bodyStart += this.minPartSize; + } + return parts; + } catch (error) { + logger.error(error); + throw error; } - return parts; } private async createMultiPartUpload() { - const createMultiPartUploadCommand = new CreateMultipartUploadCommand( - this.params - ); - const response = await this.s3client.send(createMultiPartUploadCommand); - logger.debug(response.UploadId); - return response.UploadId; + try { + const createMultiPartUploadCommand = new CreateMultipartUploadCommand( + this.params + ); + const response = await this.s3client.send(createMultiPartUploadCommand); + logger.debug(response.UploadId); + return response.UploadId; + } catch (error) { + logger.error(error); + throw error; + } } /** @@ -191,11 +199,9 @@ export class AWSS3ProviderManagedUpload { } } catch (error) { logger.error( - 'error happened while uploading a part. Cancelling the multipart upload', - error + 'Error happened while uploading a part. Cancelling the multipart upload' ); - this.cancelUpload(); - return; + throw error; } } @@ -211,31 +217,11 @@ export class AWSS3ProviderManagedUpload { const data = await this.s3client.send(completeUploadCommand); return data.Key; } catch (error) { - logger.error( - 'error happened while finishing the upload. Cancelling the multipart upload', - error - ); - this.cancelUpload(); - return; + logger.error('Error happened while finishing the upload.'); + throw error; } } - private async checkIfUploadCancelled(uploadId: string) { - if (this.cancel) { - let errorMessage = 'Upload was cancelled.'; - try { - await this.cleanup(uploadId); - } catch (error) { - errorMessage += ` ${error.message}`; - } - throw new Error(errorMessage); - } - } - - public cancelUpload() { - this.cancel = true; - } - private async cleanup(uploadId: string) { // Reset this's state this.body = null; @@ -255,7 +241,7 @@ export class AWSS3ProviderManagedUpload { const data = await this.s3client.send(new ListPartsCommand(input)); if (data && data.Parts && data.Parts.length > 0) { - throw new Error('Multi Part upload clean up failed'); + throw new Error('Multipart upload clean up failed.'); } } diff --git a/packages/storage/src/providers/axios-http-handler.ts b/packages/storage/src/providers/axios-http-handler.ts index 69c66285595..d4d0a1e3b81 100644 --- a/packages/storage/src/providers/axios-http-handler.ts +++ b/packages/storage/src/providers/axios-http-handler.ts @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is located at @@ -18,13 +18,24 @@ import axios, { AxiosRequestConfig, Method, CancelTokenSource, - AxiosTransformer, + AxiosRequestHeaders, + AxiosRequestTransformer, } from 'axios'; import { ConsoleLogger as Logger, Platform } from '@aws-amplify/core'; import { FetchHttpHandlerOptions } from '@aws-sdk/fetch-http-handler'; import * as events from 'events'; import { AWSS3ProviderUploadErrorStrings } from '../common/StorageErrorStrings'; +/** +Extending the axios interface here to make headers required, (previously, +they were not required on the type we were using, but our implementation +does not currently account for missing headers. This worked previously, +because the previous `headers` type was `any`. +*/ +interface AxiosTransformer extends Partial { + (data: any, headers: AxiosRequestHeaders): any; +} + const logger = new Logger('axios-http-handler'); export const SEND_UPLOAD_PROGRESS_EVENT = 'sendUploadProgress'; export const SEND_DOWNLOAD_PROGRESS_EVENT = 'sendDownloadProgress'; @@ -48,7 +59,7 @@ function hasErrorResponse(error: any): error is ErrorWithResponse { } const normalizeHeaders = ( - headers: Record, + headers: AxiosRequestHeaders, normalizedName: string ) => { for (const [k, v] of Object.entries(headers)) { @@ -63,7 +74,7 @@ const normalizeHeaders = ( }; export const reactNativeRequestTransformer: AxiosTransformer[] = [ - function(data, headers) { + (data: any, headers: AxiosRequestHeaders): any => { if (isBlob(data)) { normalizeHeaders(headers, 'Content-Type'); normalizeHeaders(headers, 'Accept'); @@ -151,10 +162,12 @@ export class AxiosHttpHandler implements HttpHandler { } } if (emitter) { + // TODO: Unify linting rules across JS repo axiosRequest.onUploadProgress = function(event) { emitter.emit(SEND_UPLOAD_PROGRESS_EVENT, event); logger.debug(event); }; + // TODO: Unify linting rules across JS repo axiosRequest.onDownloadProgress = function(event) { emitter.emit(SEND_DOWNLOAD_PROGRESS_EVENT, event); logger.debug(event); diff --git a/packages/storage/src/types/AWSS3Provider.ts b/packages/storage/src/types/AWSS3Provider.ts index 4ed84827e6f..b8ce6c7592e 100644 --- a/packages/storage/src/types/AWSS3Provider.ts +++ b/packages/storage/src/types/AWSS3Provider.ts @@ -12,6 +12,7 @@ import { UploadTaskProgressEvent, } from '../providers/AWSS3UploadTask'; import { UploadTask } from './Provider'; +import { ICredentials } from '@aws-amplify/core'; type ListObjectsCommandOutputContent = _Object; @@ -94,15 +95,24 @@ export type S3ProviderRemoveConfig = CommonStorageOptions & { provider?: 'AWSS3'; }; +export type S3ProviderListOutputWithToken = { + contents: S3ProviderListOutputItem[]; + nextToken: string; +}; + export type S3ProviderRemoveOutput = DeleteObjectCommandOutput; export type S3ProviderListConfig = CommonStorageOptions & { bucket?: string; - maxKeys?: number; + maxKeys?: number | 'ALL'; provider?: 'AWSS3'; identityId?: string; }; +export type S3ClientOptions = StorageOptions & { + credentials: ICredentials; +} & S3ProviderListConfig; + export interface S3ProviderListOutputItem { key: ListObjectsCommandOutputContent['Key']; eTag: ListObjectsCommandOutputContent['ETag']; diff --git a/packages/xr/CHANGELOG.md b/packages/xr/CHANGELOG.md index dfca1dc541a..8dc06b6cb3e 100644 --- a/packages/xr/CHANGELOG.md +++ b/packages/xr/CHANGELOG.md @@ -3,6 +3,110 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.50...@aws-amplify/xr@3.0.51) (2022-08-23) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.49...@aws-amplify/xr@3.0.50) (2022-08-18) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.48...@aws-amplify/xr@3.0.49) (2022-08-16) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.47...@aws-amplify/xr@3.0.48) (2022-08-01) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.46...@aws-amplify/xr@3.0.47) (2022-07-28) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.45...@aws-amplify/xr@3.0.46) (2022-07-21) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.44...@aws-amplify/xr@3.0.45) (2022-07-07) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.43...@aws-amplify/xr@3.0.44) (2022-06-18) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.42...@aws-amplify/xr@3.0.43) (2022-06-15) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.41...@aws-amplify/xr@3.0.42) (2022-05-24) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.40...@aws-amplify/xr@3.0.41) (2022-05-23) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.39...@aws-amplify/xr@3.0.40) (2022-05-12) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + +## [3.0.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.38...@aws-amplify/xr@3.0.39) (2022-05-03) + +**Note:** Version bump only for package @aws-amplify/xr + + + + + ## [3.0.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/xr@3.0.37...@aws-amplify/xr@3.0.38) (2022-04-14) **Note:** Version bump only for package @aws-amplify/xr diff --git a/packages/xr/package.json b/packages/xr/package.json index 7ba478ac1d3..bd079466f76 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/xr", - "version": "3.0.38", + "version": "3.0.51", "description": "XR category of aws-amplify", "main": "./lib/index.js", "module": "./lib-esm/index.js", @@ -41,7 +41,7 @@ }, "homepage": "https://aws-amplify.github.io/", "dependencies": { - "@aws-amplify/core": "4.5.2" + "@aws-amplify/core": "4.7.2" }, "jest": { "globals": {