diff --git a/.gitignore b/.gitignore index 255df3e152..d0b2ae737f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ codebuild_specs/debug_workflow.yml .npmrc verdaccio-logs.txt scripts/components/private_packages.ts +scripts/e2e-test-local-cluster-config.json \ No newline at end of file diff --git a/packages/amplify-e2e-core/src/utils/rds.ts b/packages/amplify-e2e-core/src/utils/rds.ts index 3dcaf69ab2..ab7a5a6005 100644 --- a/packages/amplify-e2e-core/src/utils/rds.ts +++ b/packages/amplify-e2e-core/src/utils/rds.ts @@ -4,12 +4,14 @@ import { CreateDBInstanceCommand, CreateDBInstanceCommandInput, DBInstance, - DeleteDBInstanceCommand, - waitUntilDBInstanceAvailable, CreateDBClusterMessage, - waitUntilDBClusterAvailable, DeleteDBClusterCommand, DeleteDBClusterCommandInput, + DeleteDBInstanceCommand, + DescribeDBClustersCommand, + DescribeDBInstancesCommand, + waitUntilDBClusterAvailable, + waitUntilDBInstanceAvailable, } from '@aws-sdk/client-rds'; import { RDSDataClient, ExecuteStatementCommand, ExecuteStatementCommandInput, Field } from '@aws-sdk/client-rds-data'; import generator from 'generate-password'; @@ -39,7 +41,7 @@ export type SqlEngine = 'mysql' | 'postgres'; export type RDSConfig = { identifier: string; engine: SqlEngine; - dbname: string; + dbname?: string; username: string; password?: string; region: string; @@ -55,6 +57,7 @@ export type ClusterInfo = { dbName: string; secretArn: string; dbInstance: DBInstance; + username?: string; }; const getRDSEngineType = (engine: SqlEngine): string => { @@ -241,6 +244,85 @@ export const createRDSCluster = async (config: RDSConfig): Promise } }; +/** + * Setup the test database and data in the pre-existing RDS Aurora serverless V2 cluster with one writer DB instance. Get the necessary configuration settings of the cluster and instance. + * @param identifier Cluster idenfitier. + * @param config Configuration of the database cluster. + * @param queries Initial queries to be executed. + * @returns Cluster configuration information. + */ +export const setupDataInExistingCluster = async (identifier: string, config: RDSConfig, queries: string[]): Promise => { + try { + const client = new RDSClient({ region: config.region }); + const describeClusterResponse = await client.send( + new DescribeDBClustersCommand({ Filters: [{ Name: 'db-cluster-id', Values: [identifier] }] }), + ); + if (!describeClusterResponse || !describeClusterResponse?.DBClusters[0]) { + throw Error('Specified cluster info cannot be fetched'); + } + const dbClusterObj = describeClusterResponse.DBClusters[0]; + const instances = dbClusterObj?.DBClusterMembers; + if (!instances || instances?.length === 0) { + throw new Error('No instances are present in the specified cluster'); + } + const instanceId = instances[0]?.DBInstanceIdentifier; + const describeInstanceCommand = new DescribeDBInstancesCommand({ DBInstanceIdentifier: instanceId }); + const describeInstanceResponse = await client.send(describeInstanceCommand); + if (!describeInstanceResponse || describeInstanceResponse?.DBInstances?.length === 0) { + throw Error('Specified cluster instance info cannot be fetched'); + } + + const clusterArn = dbClusterObj.DBClusterArn; + const secretArn = dbClusterObj.MasterUserSecret.SecretArn; + const defaultDbName = dbClusterObj.DatabaseName; + const dataClient = new RDSDataClient({ region: config.region }); + const sanitizedDbName = config.dbname?.replace(/[^a-zA-Z0-9_]/g, ''); + + const createDBInput: ExecuteStatementCommandInput = { + resourceArn: clusterArn, + secretArn, + sql: `create database ${sanitizedDbName}`, + database: defaultDbName, + }; + + const createDBCommand = new ExecuteStatementCommand(createDBInput); + try { + const createDBResponse = await dataClient.send(createDBCommand); + console.log('Create database response: ' + JSON.stringify(createDBResponse)); + } catch (err) { + console.log(err); + } + + // create the test tables in the test database + for (const query of queries ?? []) { + try { + const executeStatementInput: ExecuteStatementCommandInput = { + resourceArn: clusterArn, + secretArn: secretArn, + sql: query, + database: sanitizedDbName, + }; + const executeStatementResponse = await dataClient.send(new ExecuteStatementCommand(executeStatementInput)); + console.log('Run query response: ' + JSON.stringify(executeStatementResponse)); + } catch (err) { + throw new Error(`Error in creating tables in test database: ${JSON.stringify(err, null, 4)}`); + } + } + + return { + clusterArn, + endpoint: dbClusterObj.Endpoint, + port: dbClusterObj.Port, + dbName: sanitizedDbName, + dbInstance: describeInstanceResponse.DBInstances[0], + secretArn, + username: dbClusterObj.MasterUsername, + }; + } catch (error) { + console.log('Error while setting up the test data in existing cluster: ', JSON.stringify(error)); + } +}; + /** * Creates a new RDS instance using the given input configuration, runs the given queries and returns the details of the created RDS * instance. @@ -310,7 +392,6 @@ export const setupRDSInstanceAndData = async ( export const setupRDSClusterAndData = async (config: RDSConfig, queries?: string[]): Promise => { console.log(`Creating RDS ${config.engine} DB cluster with identifier ${config.identifier}`); - const dbCluster = await createRDSCluster(config); if (!dbCluster.secretArn) { @@ -319,8 +400,7 @@ export const setupRDSClusterAndData = async (config: RDSConfig, queries?: string const client = new RDSDataClient({ region: config.region }); - // create a new test database with given name - const sanitizedDbName = config.dbname.replace(/[^a-zA-Z0-9_]/g, ''); + const sanitizedDbName = config.dbname?.replace(/[^a-zA-Z0-9_]/g, ''); const createDBInput: ExecuteStatementCommandInput = { resourceArn: dbCluster.clusterArn, @@ -347,7 +427,7 @@ export const setupRDSClusterAndData = async (config: RDSConfig, queries?: string database: sanitizedDbName, }; const executeStatementResponse = await client.send(new ExecuteStatementCommand(executeStatementInput)); - console.log('Create table response: ' + JSON.stringify(executeStatementResponse)); + console.log('Run query response: ' + JSON.stringify(executeStatementResponse)); } catch (err) { throw new Error(`Error in creating tables in test database: ${JSON.stringify(err, null, 4)}`); } @@ -395,7 +475,7 @@ export const deleteDBInstance = async (identifier: string, region: string): Prom // ); } catch (error) { console.log(error); - throw new Error(`Error in deleting RDS instance: ${error.response.json}`); + throw new Error(`Error in deleting RDS instance: ${JSON.stringify(error)}`); } }; @@ -422,7 +502,7 @@ export const deleteDBCluster = async (identifier: string, region: string): Promi await client.send(command); } catch (error) { console.log(error); - throw new Error(`Error in deleting RDS cluster ${identifier}: ${error.response.json}`); + throw new Error(`Error in deleting RDS cluster ${identifier}: ${JSON.stringify(error)}`); } }; diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-ddbprimary-sqlrelated.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-ddbprimary-sqlrelated.test.ts index 720d7db167..d7e51b5e5f 100644 --- a/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-ddbprimary-sqlrelated.test.ts +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-ddbprimary-sqlrelated.test.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import * as generator from 'generate-password'; -import { createNewProjectDir, deleteProjectDir, generateDBName } from 'amplify-category-api-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core'; import { DDB_AMPLIFY_MANAGED_DATASOURCE_STRATEGY } from '@aws-amplify/graphql-transformer-core'; import { cdkDeploy, cdkDestroy, initCDKProject } from '../../../commands'; import { SqlDatabaseDetails, SqlDatatabaseController } from '../../../sql-datatabase-controller'; @@ -23,7 +23,6 @@ describe('PostgreSQL tables with UUID primary keys', () => { const region = process.env.CLI_REGION ?? 'us-west-2'; const baseProjFolderName = path.basename(__filename, '.test.ts'); - const dbname = generateDBName(); const [dbUsername, dbIdentifier] = generator.generateMultiple(2); let dbDetails: SqlDatabaseDetails; @@ -46,7 +45,6 @@ describe('PostgreSQL tables with UUID primary keys', () => { { identifier: dbIdentifier, engine: 'postgres', - dbname, username: dbUsername, region, }, diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-ddbrelated.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-ddbrelated.test.ts index 32d6e9156c..205a860a7d 100644 --- a/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-ddbrelated.test.ts +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-ddbrelated.test.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import * as generator from 'generate-password'; -import { createNewProjectDir, deleteProjectDir, generateDBName } from 'amplify-category-api-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core'; import { DDB_AMPLIFY_MANAGED_DATASOURCE_STRATEGY } from '@aws-amplify/graphql-transformer-core'; import { cdkDeploy, cdkDestroy, initCDKProject } from '../../../commands'; import { SqlDatabaseDetails, SqlDatatabaseController } from '../../../sql-datatabase-controller'; @@ -23,7 +23,6 @@ describe('PostgreSQL tables with UUID primary keys', () => { const region = process.env.CLI_REGION ?? 'us-west-2'; const baseProjFolderName = path.basename(__filename, '.test.ts'); - const dbname = generateDBName(); const [dbUsername, dbIdentifier] = generator.generateMultiple(2); let dbDetails: SqlDatabaseDetails; @@ -46,7 +45,6 @@ describe('PostgreSQL tables with UUID primary keys', () => { { identifier: dbIdentifier, engine: 'postgres', - dbname, username: dbUsername, region, }, diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-sqlrelated.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-sqlrelated.test.ts index 934201aaaf..2e8f61532d 100644 --- a/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-sqlrelated.test.ts +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/relationships/postgres-uuid-pk/uuid-pk-sqlprimary-sqlrelated.test.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import * as generator from 'generate-password'; -import { createNewProjectDir, deleteProjectDir, generateDBName } from 'amplify-category-api-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core'; import { cdkDeploy, cdkDestroy, initCDKProject } from '../../../commands'; import { SqlDatabaseDetails, SqlDatatabaseController } from '../../../sql-datatabase-controller'; import { TestDefinition, dbDetailsToModelDataSourceStrategy, writeStackConfig, writeTestDefinitions } from '../../../utils'; @@ -22,7 +22,6 @@ describe('PostgreSQL tables with UUID primary keys', () => { const region = process.env.CLI_REGION ?? 'us-west-2'; const baseProjFolderName = path.basename(__filename, '.test.ts'); - const dbname = generateDBName(); const [dbUsername, dbIdentifier] = generator.generateMultiple(2); let dbDetails: SqlDatabaseDetails; @@ -45,7 +44,6 @@ describe('PostgreSQL tables with UUID primary keys', () => { { identifier: dbIdentifier, engine: 'postgres', - dbname, username: dbUsername, region, }, diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-canary.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-canary.test.ts index 8453be4e06..2ec6411c9e 100644 --- a/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-canary.test.ts +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-canary.test.ts @@ -1,4 +1,4 @@ -import { createNewProjectDir, deleteProjectDir, generateDBName } from 'amplify-category-api-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core'; import generator from 'generate-password'; import { getResourceNamesForStrategyName } from '@aws-amplify/graphql-transformer-core'; import { SqlDatatabaseController } from '../sql-datatabase-controller'; @@ -14,7 +14,6 @@ describe('Canary using Postgres lambda model datasource strategy', () => { // sufficient password length that meets the requirements for RDS cluster/instance const [username, password, identifier] = generator.generateMultiple(3, { length: 11 }); const region = process.env.CLI_REGION ?? 'us-west-2'; - const dbname = generateDBName(); const engine = 'postgres'; const databaseController: SqlDatatabaseController = new SqlDatatabaseController( @@ -22,7 +21,6 @@ describe('Canary using Postgres lambda model datasource strategy', () => { { identifier, engine, - dbname, username, password, region, @@ -33,7 +31,9 @@ describe('Canary using Postgres lambda model datasource strategy', () => { const resourceNames = getResourceNamesForStrategyName(strategyName); beforeAll(async () => { + console.time('sql-pg-canary test setup'); await databaseController.setupDatabase(); + console.timeEnd('sql-pg-canary test setup'); }); afterAll(async () => { diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-models.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-models.test.ts index 7034bd5896..32fbb00d49 100644 --- a/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-models.test.ts +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/sql-pg-models.test.ts @@ -1,4 +1,4 @@ -import { createNewProjectDir, deleteProjectDir, generateDBName } from 'amplify-category-api-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core'; import generator from 'generate-password'; import { getResourceNamesForStrategyName } from '@aws-amplify/graphql-transformer-core'; import { SqlDatatabaseController } from '../sql-datatabase-controller'; @@ -15,7 +15,6 @@ describe('CDK GraphQL Transformer deployments with Postgres SQL datasources', () // sufficient password length that meets the requirements for RDS cluster/instance const [username, password, identifier] = generator.generateMultiple(3, { length: 11 }); const region = process.env.CLI_REGION ?? 'us-west-2'; - const dbname = generateDBName(); const engine = 'postgres'; const databaseController: SqlDatatabaseController = new SqlDatatabaseController( @@ -23,7 +22,6 @@ describe('CDK GraphQL Transformer deployments with Postgres SQL datasources', () { identifier, engine, - dbname, username, password, region, diff --git a/packages/amplify-graphql-api-construct-tests/src/sql-datatabase-controller.ts b/packages/amplify-graphql-api-construct-tests/src/sql-datatabase-controller.ts index 0e724b2464..accfc2a938 100644 --- a/packages/amplify-graphql-api-construct-tests/src/sql-datatabase-controller.ts +++ b/packages/amplify-graphql-api-construct-tests/src/sql-datatabase-controller.ts @@ -4,6 +4,7 @@ import { SqlModelDataSourceDbConnectionConfig, ModelDataSourceStrategySqlDbType import { deleteSSMParameters, deleteDbConnectionConfigWithSecretsManager, + deleteDBCluster, deleteDBInstance, extractVpcConfigFromDbInstance, RDSConfig, @@ -13,16 +14,18 @@ import { storeDbConnectionConfig, storeDbConnectionStringConfig, storeDbConnectionConfigWithSecretsManager, - deleteDBCluster, - isOptInRegion, isDataAPISupported, + isCI, + generateDBName, + setupDataInExistingCluster, } from 'amplify-category-api-e2e-core'; -import { SecretsManagerClient, CreateSecretCommand, DeleteSecretCommand, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; +import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; import { isSqlModelDataSourceSecretsManagerDbConnectionConfig, isSqlModelDataSourceSsmDbConnectionConfig, isSqlModelDataSourceSsmDbConnectionStringConfig, } from '@aws-amplify/graphql-transformer-interfaces'; +import { getClusterIdentifier } from './utils/sql-local-testing'; export interface SqlDatabaseDetails { dbConfig: { @@ -51,20 +54,35 @@ export interface SqlDatabaseDetails { export class SqlDatatabaseController { private databaseDetails: SqlDatabaseDetails | undefined; private useDataAPI: boolean; + private enableLocalTesting: boolean; - constructor(private readonly setupQueries: Array, private readonly options: RDSConfig) { + constructor(private readonly setupQueries: Array, private options: RDSConfig) { // Data API is not supported in opted-in regions if (options.engine === 'postgres' && isDataAPISupported(options.region)) { this.useDataAPI = true; + this.enableLocalTesting = !isCI() && getClusterIdentifier(options.region, options.engine) !== undefined; } else { this.useDataAPI = false; } + + // If database name not manually set, provide and sanitize the config dbname + if (!options.dbname || options.dbname.length == 0 || this.enableLocalTesting) { + this.options.dbname = generateDBName().replace(/[^a-zA-Z0-9_]/g, ''); + } } setupDatabase = async (): Promise => { let dbConfig; + if (this.useDataAPI) { - dbConfig = await setupRDSClusterAndData(this.options, this.setupQueries); + if (this.enableLocalTesting) { + const identifier = getClusterIdentifier(this.options.region, this.options.engine); + dbConfig = await setupDataInExistingCluster(identifier, this.options, this.setupQueries); + this.options.username = dbConfig.username; + this.options.dbname = dbConfig.dbName; + } else { + dbConfig = await setupRDSClusterAndData(this.options, this.setupQueries); + } } else { dbConfig = await setupRDSInstanceAndData(this.options, this.setupQueries); } @@ -81,7 +99,7 @@ export class SqlDatatabaseController { }; console.log(`Stored db connection config in Secrets manager: ${JSON.stringify(dbConnectionConfigSecretsManager)}`); - if (this.useDataAPI || !this.options.password) { + if (this.useDataAPI || !this.options.password || this.enableLocalTesting) { const secretArn = dbConfig.secretArn; const secretsManagerClient = new SecretsManagerClient({ region: this.options.region }); const secretManagerCommand = new GetSecretValueCommand({ @@ -94,14 +112,14 @@ export class SqlDatatabaseController { } this.options.password = managedPassword; } - - const { secretArn: secretArnWithCustomKey, keyArn } = await storeDbConnectionConfigWithSecretsManager({ + const { secretArn: secretArnWithCustomKey, keyArn: keyArn } = await storeDbConnectionConfigWithSecretsManager({ region: this.options.region, username: this.options.username, password: this.options.password, - secretName: `${this.options.identifier}-secret-custom-key`, + secretName: `${this.options.identifier}-${this.options.dbname}-secret-custom-key`, useCustomEncryptionKey: true, }); + if (!secretArnWithCustomKey) { throw new Error('Failed to store db connection config for secrets manager'); } @@ -114,7 +132,7 @@ export class SqlDatatabaseController { }; console.log(`Stored db connection config in Secrets manager: ${JSON.stringify(dbConnectionConfigSecretsManagerCustomKey)}`); - const pathPrefix = `/${this.options.identifier}/test`; + const pathPrefix = `/${this.options.identifier}/${this.options.dbname}/test`; const engine = this.options.engine; const dbConnectionConfigSSM = await storeDbConnectionConfig({ region: this.options.region, @@ -178,20 +196,20 @@ export class SqlDatatabaseController { connectionUriMultiple: dbConnectionStringConfigMultiple, }, }; - return this.databaseDetails; }; cleanupDatabase = async (): Promise => { if (!this.databaseDetails) { - // Database has not been set up. return; } - if (this.useDataAPI) { - await deleteDBCluster(this.options.identifier, this.options.region); - } else { - await deleteDBInstance(this.options.identifier, this.options.region); + if (!this.enableLocalTesting) { + if (this.useDataAPI) { + await deleteDBCluster(this.options.identifier, this.options.region); + } else { + await deleteDBInstance(this.options.identifier, this.options.region); + } } const { connectionConfigs } = this.databaseDetails; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/sql-local-testing.ts b/packages/amplify-graphql-api-construct-tests/src/utils/sql-local-testing.ts new file mode 100644 index 0000000000..b8285a235c --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/utils/sql-local-testing.ts @@ -0,0 +1,29 @@ +import path from 'path'; +import * as fs from 'fs-extra'; +import { SqlEngine } from 'amplify-category-api-e2e-core'; + +export const getClusterIdentifier = (region: string, engine: SqlEngine): string | undefined => { + const repoRoot = path.join(__dirname, '..', '..', '..', '..'); + const localClusterPath = path.join(repoRoot, 'scripts', 'e2e-test-local-cluster-config.json'); + if (!fs.existsSync(localClusterPath)) { + return; + } + try { + const localClustersObject = JSON.parse(fs.readFileSync(localClusterPath, 'utf-8')); + const regionObject = localClustersObject[region]; + if (!regionObject || regionObject?.length === 0) { + return; + } + + const clusterConfig = regionObject[0]?.dbConfig; + if (!clusterConfig || !clusterConfig.engine) { + return; + } + // Get the config identifier and connection URI + const identifier = clusterConfig.identifier; + return identifier; + } catch (err) { + // cannot get local cluster information + return; + } +}; diff --git a/scripts/e2e-test-local-cluster-config.sample.json b/scripts/e2e-test-local-cluster-config.sample.json new file mode 100644 index 0000000000..6ec17aa657 --- /dev/null +++ b/scripts/e2e-test-local-cluster-config.sample.json @@ -0,0 +1,18 @@ +{ + "us-east-1": [ + { + "dbConfig": { + "identifier": "IDENTIFIER_VALUE", + "engine": "ENGINE_TYPE" + } + } + ], + "us-west-2": [ + { + "dbConfig": { + "identifier": "IDENTIFIER_VALUE", + "engine": "ENGINE_TYPE" + } + } + ] +} diff --git a/yarn.lock b/yarn.lock index 556232fdcb..d5456e5e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8970,11 +8970,16 @@ async@^2.6.4: dependencies: lodash "^4.17.14" -async@^3.2.0, async@^3.2.3, async@^3.2.4: +async@^3.2.0, async@^3.2.4: version "3.2.5" resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== +async@^3.2.3: + version "3.2.6" + resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynciterator.prototype@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62"