From 633e22e477c79882ee3614bcaca595fa051cb139 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Tue, 27 Aug 2024 19:27:39 +0200 Subject: [PATCH] relax object accumulator to accumulate parts with different versions with same major version --- .changeset/hot-tigers-speak.md | 6 ++ package-lock.json | 1 + .../unified_client_config_generator.test.ts | 90 ++++++++++++++++++ packages/platform-core/package.json | 1 + .../src/object_accumulator.test.ts | 92 ++++++++++++++++++- .../platform-core/src/object_accumulator.ts | 24 +++-- 6 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 .changeset/hot-tigers-speak.md diff --git a/.changeset/hot-tigers-speak.md b/.changeset/hot-tigers-speak.md new file mode 100644 index 0000000000..480b6b39e5 --- /dev/null +++ b/.changeset/hot-tigers-speak.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/client-config': patch +'@aws-amplify/platform-core': patch +--- + +relax object accumulator to accumulate parts with different versions with same major version diff --git a/package-lock.json b/package-lock.json index 8c58b5ce5b..438ac0856d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25331,6 +25331,7 @@ "@aws-sdk/client-sts": "^3.624.0", "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", + "semver": "^7.6.3", "uuid": "^9.0.1", "zod": "^3.22.2" }, diff --git a/packages/client-config/src/unified_client_config_generator.test.ts b/packages/client-config/src/unified_client_config_generator.test.ts index a391215eab..9f1734ca2a 100644 --- a/packages/client-config/src/unified_client_config_generator.test.ts +++ b/packages/client-config/src/unified_client_config_generator.test.ts @@ -245,6 +245,96 @@ void describe('UnifiedClientConfigGenerator', () => { assert.deepStrictEqual(result, expectedClientConfig); }); + void it('can generate client config for apps using custom config of V1', async () => { + const stubOutput: UnifiedBackendOutput = { + [platformOutputKey]: { + version: '1', + payload: { + deploymentType: 'branch', + region: 'us-east-1', + }, + }, + [authOutputKey]: { + version: '1', + payload: { + identityPoolId: 'testIdentityPoolId', + userPoolId: 'testUserPoolId', + webClientId: 'testWebClientId', + authRegion: 'us-east-1', + passwordPolicyMinLength: '8', + passwordPolicyRequirements: + '["REQUIRES_NUMBERS","REQUIRES_LOWERCASE","REQUIRES_UPPERCASE"]', + mfaTypes: '["SMS","TOTP"]', + mfaConfiguration: 'OPTIONAL', + verificationMechanisms: '["email","phone_number"]', + usernameAttributes: '["email"]', + signupAttributes: '["email"]', + allowUnauthenticatedIdentities: 'true', + }, + }, + [customOutputKey]: { + version: '1', + payload: { + customOutputs: JSON.stringify({ + version: '1', // Uses old configuration + custom: { + output1: 'val1', + output2: 'val2', + }, + }), + }, + }, + }; + const outputRetrieval = mock.fn(async () => stubOutput); + const modelSchemaAdapter = new ModelIntrospectionSchemaAdapter( + stubClientProvider + ); + + mock.method( + modelSchemaAdapter, + 'getModelIntrospectionSchemaFromS3Uri', + () => undefined + ); + const configContributors = new ClientConfigContributorFactory( + modelSchemaAdapter + ).getContributors('1.1'); //Generate with new configuration format + const clientConfigGenerator = new UnifiedClientConfigGenerator( + outputRetrieval, + configContributors + ); + const result = await clientConfigGenerator.generateClientConfig(); + const expectedClientConfig: ClientConfig = { + auth: { + user_pool_id: 'testUserPoolId', + aws_region: 'us-east-1', + user_pool_client_id: 'testWebClientId', + identity_pool_id: 'testIdentityPoolId', + mfa_methods: ['SMS', 'TOTP'], + standard_required_attributes: ['email'], + username_attributes: ['email'], + user_verification_types: ['email', 'phone_number'], + mfa_configuration: 'OPTIONAL', + + password_policy: { + min_length: 8, + require_lowercase: true, + require_numbers: true, + require_symbols: false, + require_uppercase: true, + }, + + unauthenticated_identities_enabled: true, + }, + custom: { + output1: 'val1', + output2: 'val2', + }, + version: '1.1', // The max version prevails + }; + + assert.deepStrictEqual(result, expectedClientConfig); + }); + void it('throws user error if there are overlapping values', async () => { const customOutputs = { auth: { user_pool_id: 'overrideUserPoolId' }, diff --git a/packages/platform-core/package.json b/packages/platform-core/package.json index 2db7a37f2b..ac0564aab8 100644 --- a/packages/platform-core/package.json +++ b/packages/platform-core/package.json @@ -28,6 +28,7 @@ "@aws-sdk/client-sts": "^3.624.0", "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", + "semver": "^7.6.3", "uuid": "^9.0.1", "zod": "^3.22.2" } diff --git a/packages/platform-core/src/object_accumulator.test.ts b/packages/platform-core/src/object_accumulator.test.ts index 87de9be40f..fff6307304 100644 --- a/packages/platform-core/src/object_accumulator.test.ts +++ b/packages/platform-core/src/object_accumulator.test.ts @@ -128,6 +128,96 @@ void describe('Object accumulator', () => { }); }); + void it('should merge two objects with same major version 1.1 and 1.2', () => { + const object1 = { + myVersionKey: '1.2', + a1: 'valueA1', + b1: { + c1: 'valueC1', + d1: { + e1: 'valueE1', + }, + }, + }; + const object2 = { + myVersionKey: '1.1', + a2: 'valueA2', + b2: { + c2: 'valueC2', + d2: { + e2: 'valueE2', + }, + }, + }; + const accumulatedObject = new ObjectAccumulator({}, 'myVersionKey') + .accumulate(object1) + .accumulate(object2) + .getAccumulatedObject(); + + assert.deepEqual(accumulatedObject, { + myVersionKey: '1.2', + a1: 'valueA1', + a2: 'valueA2', + b1: { + c1: 'valueC1', + d1: { + e1: 'valueE1', + }, + }, + b2: { + c2: 'valueC2', + d2: { + e2: 'valueE2', + }, + }, + }); + }); + + void it('should merge two objects with same major version 1.1 and 1', () => { + const object1 = { + myVersionKey: '1.1', + a1: 'valueA1', + b1: { + c1: 'valueC1', + d1: { + e1: 'valueE1', + }, + }, + }; + const object2 = { + myVersionKey: '1.0', + a2: 'valueA2', + b2: { + c2: 'valueC2', + d2: { + e2: 'valueE2', + }, + }, + }; + const accumulatedObject = new ObjectAccumulator({}, 'myVersionKey') + .accumulate(object1) + .accumulate(object2) + .getAccumulatedObject(); + + assert.deepEqual(accumulatedObject, { + myVersionKey: '1.1', + a1: 'valueA1', + a2: 'valueA2', + b1: { + c1: 'valueC1', + d1: { + e1: 'valueE1', + }, + }, + b2: { + c2: 'valueC2', + d2: { + e2: 'valueE2', + }, + }, + }); + }); + void it('should throw on property override attempt', () => { assert.throws( () => { @@ -153,7 +243,7 @@ void describe('Object accumulator', () => { ); }); - void it('should throw on version mismatch of objects', () => { + void it('should throw on major version mismatch of objects', () => { assert.throws( () => { new ObjectAccumulator({}) // using default version key of `version` diff --git a/packages/platform-core/src/object_accumulator.ts b/packages/platform-core/src/object_accumulator.ts index b7b6052e34..1ef63d3d7f 100644 --- a/packages/platform-core/src/object_accumulator.ts +++ b/packages/platform-core/src/object_accumulator.ts @@ -1,6 +1,6 @@ import { DeepPartialAmplifyGeneratedConfigs } from '@aws-amplify/plugin-types'; import mergeWith from 'lodash.mergewith'; - +import semver from 'semver'; /** * This error is thrown when there's a collision in the object keys */ @@ -58,11 +58,23 @@ export class ObjectAccumulator { return existingValue.concat(incomingValue); } if (existingValue && typeof existingValue !== 'object') { - if (key === this.versionKey && existingValue !== incomingValue) { - throw new ObjectAccumulatorVersionMismatchError( - existingValue, - incomingValue - ); + if (key === this.versionKey) { + const incomingVersion = semver.coerce(incomingValue); + const existingVersion = semver.coerce(existingValue); + if (incomingVersion && existingVersion) { + // Only throw if the major version is not equal + if (incomingVersion.major !== existingVersion.major) { + throw new ObjectAccumulatorVersionMismatchError( + existingValue, + incomingValue + ); + } else { + // We always get the max version to persist in the accumulated object + return semver.gte(incomingVersion, existingVersion) + ? incomingValue + : existingValue; + } + } } else if (key !== this.versionKey) { throw new ObjectAccumulatorPropertyAlreadyExistsError( key,