From 4c7efb2d37f190e3cd8d0b653a07c6edc8bd48b7 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Wed, 10 Jul 2024 16:18:40 -0700 Subject: [PATCH 01/31] update package-lock.json --- package-lock.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3ac4b6a83e0..eeaaa9d0af1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27795,7 +27795,7 @@ }, "packages/backend-data": { "name": "@aws-amplify/backend-data", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^1.1.0", @@ -27831,7 +27831,7 @@ }, "packages/backend-function": { "name": "@aws-amplify/backend-function", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^1.1.0", @@ -28249,7 +28249,7 @@ }, "packages/integration-tests": { "name": "@aws-amplify/integration-tests", - "version": "0.5.5", + "version": "0.5.6", "license": "Apache-2.0", "devDependencies": { "@apollo/client": "^3.10.1", @@ -28258,6 +28258,7 @@ "@aws-amplify/backend-secret": "^1.0.0", "@aws-amplify/client-config": "^1.0.3", "@aws-amplify/data-schema": "^1.0.0", + "@aws-amplify/deployed-backend-client": "^1.0.2", "@aws-amplify/platform-core": "^1.0.1", "@aws-sdk/client-accessanalyzer": "^3.465.0", "@aws-sdk/client-amplify": "^3.465.0", @@ -28500,7 +28501,7 @@ }, "packages/schema-generator": { "name": "@aws-amplify/schema-generator", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "@aws-amplify/graphql-schema-generator": "^0.9.2", From 02dbdba3a6b3c61fff7e4ef8e400e408f6f666d6 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Wed, 10 Jul 2024 16:31:58 -0700 Subject: [PATCH 02/31] Prettier code style --- packages/auth-construct/src/construct.test.ts | 139 +++++++++++++++++- packages/auth-construct/src/construct.ts | 70 ++++++++- packages/auth-construct/src/types.ts | 54 ++++++- 3 files changed, 260 insertions(+), 3 deletions(-) diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index 2b5465cd862..80675751b10 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -7,9 +7,15 @@ import { BackendOutputEntry, BackendOutputStorageStrategy, } from '@aws-amplify/plugin-types'; -import { CfnUserPoolClient, ProviderAttribute } from 'aws-cdk-lib/aws-cognito'; +import { + CfnUserPoolClient, + CustomAttributeConfig, + ICustomAttribute, + ProviderAttribute, +} from 'aws-cdk-lib/aws-cognito'; import { authOutputKey } from '@aws-amplify/backend-output-schemas'; import { DEFAULTS } from './defaults.js'; +import { CustomAttributes } from './types'; const googleClientId = 'googleClientId'; const googleClientSecret = 'googleClientSecret'; @@ -601,6 +607,18 @@ void describe('Auth construct', () => { familyName: { required: true, }, + 'custom:display_name': { + dataType: 'String', + mutable: true, + maxLen: 100, + minLen: 0, + }, + 'custom:tenant_id': { + dataType: 'Number', + mutable: false, + max: 66, + min: 1, + }, }, }); const template = Template.fromStack(stack); @@ -621,6 +639,24 @@ void describe('Auth construct', () => { Name: 'family_name', Required: true, }, + { + AttributeDataType: 'String', + Name: 'display_name', + Mutable: true, + StringAttributeConstraints: { + MaxLength: '100', + MinLength: '0', + }, + }, + { + AttributeDataType: 'Number', + Name: 'tenant_id', + Mutable: false, + NumberAttributeConstraints: { + MaxValue: '66', + MinValue: '1', + }, + }, ], }); }); @@ -2572,4 +2608,105 @@ void describe('Auth construct', () => { assert.equal(name.startsWith(expectedPrefix), true); }); }); + + void describe('bindCustomAttribute', () => { + let auth: AmplifyAuth; + void beforeEach(() => { + const app = new App(); + const stack = new Stack(app); + auth = new AmplifyAuth(stack, 'test'); + }); + + void it('should bind string attribute correctly', () => { + const key = 'test_string'; + const attribute: CustomAttributes = { + dataType: 'String', + mutable: true, + minLen: 3, + maxLen: 10, + }; + const expected: Omit = { + dataType: 'String', + mutable: true, + stringConstraints: { + minLen: 3, + maxLen: 10, + }, + numberConstraints: undefined, + }; + const result = auth.bindCustomAttribute(key, attribute); + assert.deepEqual(result, { ...expected, bind: result.bind }); + // Test bind function separately + const bindResult = result.bind(); + assert.deepEqual(bindResult, expected); + }); + + void it('should bind number attribute correctly', () => { + const key = 'test_number'; + const attribute: CustomAttributes = { + dataType: 'Number', + mutable: false, + min: 1, + max: 100, + }; + const expected: Omit = { + dataType: 'Number', + mutable: false, + stringConstraints: undefined, + numberConstraints: { + min: 1, + max: 100, + }, + }; + const result = auth.bindCustomAttribute(key, attribute); + assert.deepEqual(result, { ...expected, bind: result.bind }); + // Test bind function separately + const bindResult = result.bind(); + assert.deepEqual(bindResult, expected); + }); + + void it('should handle missing constraints correctly', () => { + const key = 'test_string_no_constraints'; + const attribute: CustomAttributes = { + dataType: 'String', + mutable: true, + }; + const expected: Omit = { + dataType: 'String', + mutable: true, + stringConstraints: { + minLen: undefined, + maxLen: undefined, + }, + numberConstraints: undefined, + }; + const result = auth.bindCustomAttribute(key, attribute); + assert.deepEqual(result, { ...expected, bind: result.bind }); + // Test bind function separately + const bindResult = result.bind(); + assert.deepEqual(bindResult, expected); + }); + + void it('should set mutable to true when not defined', () => { + const key = 'test_string_no_mutable'; + const attribute: CustomAttributes = { + dataType: 'String', + }; + const expected: Omit = { + dataType: 'String', + mutable: true, + stringConstraints: { + minLen: undefined, + maxLen: undefined, + }, + numberConstraints: undefined, + }; + const result = auth.bindCustomAttribute(key, attribute); + assert.deepEqual(result, { ...expected, bind: result.bind }); + + // Test bind function separately + const bindResult = result.bind(); + assert.deepEqual(bindResult, expected); + }); + }); }); diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index a6f467300c5..5bd17d8e8df 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -16,11 +16,14 @@ import { CfnUserPoolClient, CfnUserPoolGroup, CfnUserPoolIdentityProvider, + CustomAttributeConfig, + ICustomAttribute, Mfa, MfaSecondFactor, OAuthScope, OidcAttributeRequestMethod, ProviderAttribute, + StandardAttribute, UserPool, UserPoolClient, UserPoolDomain, @@ -38,6 +41,7 @@ import { AuthOutput, authOutputKey } from '@aws-amplify/backend-output-schemas'; import { AttributeMapping, AuthProps, + CustomAttributes, EmailLoginSettings, ExternalProviderOptions, } from './types.js'; @@ -214,6 +218,37 @@ export class AmplifyAuth ); } + /** + * Define bindCustomAttribute to meet requirements of the Cognito API to call the bind method + */ + public bindCustomAttribute = ( + key: string, + attribute: CustomAttributes + ): CustomAttributeConfig & ICustomAttribute => { + const config: CustomAttributeConfig = { + dataType: attribute.dataType, + mutable: attribute.mutable ?? true, + stringConstraints: + attribute.dataType === 'String' + ? { + minLen: attribute.minLen, + maxLen: attribute.maxLen, + } + : undefined, + numberConstraints: + attribute.dataType === 'Number' + ? { + min: attribute.min, + max: attribute.max, + } + : undefined, + }; + return { + ...config, + bind: () => config, + }; + }; + /** * Create Auth/UnAuth Roles * @returns DefaultRoles @@ -423,8 +458,41 @@ export class AmplifyAuth standardAttributes: { email: DEFAULTS.IS_REQUIRED_ATTRIBUTE.email(emailEnabled), phoneNumber: DEFAULTS.IS_REQUIRED_ATTRIBUTE.phoneNumber(phoneEnabled), - ...(props.userAttributes ? props.userAttributes : {}), + ...(props.userAttributes + ? Object.entries(props.userAttributes).reduce( + (acc: { [key: string]: StandardAttribute }, [key, value]) => { + if (!key.startsWith('custom:')) { + acc[key] = value; + } + return acc; + }, + {} + ) + : {}), }, + customAttributes: { + ...(props.userAttributes + ? Object.entries(props.userAttributes).reduce( + ( + acc: { + [key: string]: CustomAttributeConfig & ICustomAttribute; + }, + [key, value] + ) => { + if (key.startsWith('custom:')) { + const attributeKey = key.replace(/^(custom:|User\.?)/i, ''); + acc[attributeKey] = this.bindCustomAttribute( + attributeKey, + value + ); + } + return acc; + }, + {} + ) + : {}), + }, + selfSignUpEnabled: DEFAULTS.ALLOW_SELF_SIGN_UP, mfa: mfaMode, mfaMessage: this.getMFAMessage(props.multifactor), diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index 8cc93b4fefb..d54ecb791ed 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -3,7 +3,9 @@ import { triggerEvents } from './trigger_events.js'; import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { AuthOutput } from '@aws-amplify/backend-output-schemas'; import { + NumberAttributeConstraints, StandardAttributes, + StringAttributeConstraints, UserPoolIdentityProviderSamlMetadata, } from 'aws-cdk-lib/aws-cognito'; export type VerificationEmailWithLink = { @@ -327,6 +329,56 @@ export type ExternalProviderOptions = { */ export type TriggerEvent = (typeof triggerEvents)[number]; +/** + * CustomAttributeBase is a type that represents the base properties for a custom attribute + */ +export type CustomAttributeBase = { + /** + * @default {true} + */ + mutable?: boolean; +}; +/** + * CustomAttributeString represents a custom attribute of type string. + */ +export type CustomAttributeString = CustomAttributeBase & + StringAttributeConstraints & { + dataType: 'String'; + }; +/** + * CustomAttributeNumber represents a custom attribute of type number. + */ +export type CustomAttributeNumber = CustomAttributeBase & + NumberAttributeConstraints & { + dataType: 'Number'; + }; +/** + * CustomAttributeBoolean represents a custom attribute of type boolean. + */ +export type CustomAttributeBoolean = CustomAttributeBase & { + dataType: 'Boolean'; +}; +/** + * CustomAttributeDateTime represents a custom attribute of type dataTime. + */ +export type CustomAttributeDateTime = CustomAttributeBase & { + dataType: 'DateTime'; +}; +/** + * CustomAttributes is a union type that represents all the different types of custom attributes. + */ +export type CustomAttributes = + | CustomAttributeString + | CustomAttributeNumber + | CustomAttributeBoolean + | CustomAttributeDateTime; +/** + * UserAttributes represents the combined attributes of a user, including + * standard attributes and any number of custom attributes defined with a 'custom:' prefix. + */ +export type UserAttributes = StandardAttributes & + Record<`custom:${string}`, CustomAttributes>; + /** * Input props for the AmplifyAuth construct */ @@ -362,7 +414,7 @@ export type AuthProps = { * The set of attributes that are required for every user in the user pool. Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html * @default - email/phone will be added as required user attributes if they are included as login methods */ - userAttributes?: StandardAttributes; + userAttributes?: UserAttributes; /** * Configure whether users can or are required to use multifactor (MFA) to sign in. */ From df3452442172661c129d66c9b11e0eadda27b68b Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Wed, 10 Jul 2024 16:33:30 -0700 Subject: [PATCH 03/31] add changeset --- .changeset/blue-turkeys-trade.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-turkeys-trade.md diff --git a/.changeset/blue-turkeys-trade.md b/.changeset/blue-turkeys-trade.md new file mode 100644 index 00000000000..5d3bf27bde1 --- /dev/null +++ b/.changeset/blue-turkeys-trade.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/auth-construct': minor +--- + +adding customAttributes into userAttributes From 847b4bd2d504daa1e19d5762f3489a09592fe40d Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 11:47:38 -0700 Subject: [PATCH 04/31] update construct --- packages/auth-construct/src/construct.ts | 59 +++++++++++------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 5bd17d8e8df..7256070dc5e 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -439,6 +439,32 @@ export class AmplifyAuth ); } + const { standardAttributes, customAttributes } = Object.entries( + props.userAttributes ?? {} + ).reduce( + ( + acc: { + standardAttributes: { [key: string]: StandardAttribute }; + customAttributes: { + [key: string]: CustomAttributeConfig & ICustomAttribute; + }; + }, + [key, value] + ) => { + if (key.startsWith('custom:')) { + const attributeKey = key.replace(/^(custom:|User\.?)/i, ''); + acc.customAttributes[attributeKey] = this.bindCustomAttribute( + attributeKey, + value + ); + } else { + acc.standardAttributes[key] = value; + } + return acc; + }, + { standardAttributes: {}, customAttributes: {} } + ); + const userPoolProps: UserPoolProps = { signInCaseSensitive: DEFAULTS.SIGN_IN_CASE_SENSITIVE, signInAliases: { @@ -458,39 +484,10 @@ export class AmplifyAuth standardAttributes: { email: DEFAULTS.IS_REQUIRED_ATTRIBUTE.email(emailEnabled), phoneNumber: DEFAULTS.IS_REQUIRED_ATTRIBUTE.phoneNumber(phoneEnabled), - ...(props.userAttributes - ? Object.entries(props.userAttributes).reduce( - (acc: { [key: string]: StandardAttribute }, [key, value]) => { - if (!key.startsWith('custom:')) { - acc[key] = value; - } - return acc; - }, - {} - ) - : {}), + ...standardAttributes, }, customAttributes: { - ...(props.userAttributes - ? Object.entries(props.userAttributes).reduce( - ( - acc: { - [key: string]: CustomAttributeConfig & ICustomAttribute; - }, - [key, value] - ) => { - if (key.startsWith('custom:')) { - const attributeKey = key.replace(/^(custom:|User\.?)/i, ''); - acc[attributeKey] = this.bindCustomAttribute( - attributeKey, - value - ); - } - return acc; - }, - {} - ) - : {}), + ...customAttributes, }, selfSignUpEnabled: DEFAULTS.ALLOW_SELF_SIGN_UP, From d7b95464a4bb69a9e6150f191433223cc0f221d2 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 12:11:29 -0700 Subject: [PATCH 05/31] update API.md --- packages/auth-construct/API.md | 11 ++++++++++- packages/auth-construct/src/index.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index 2c14e1f0ad2..8bf044b690f 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -9,9 +9,13 @@ import { AuthResources } from '@aws-amplify/plugin-types'; import { aws_cognito } from 'aws-cdk-lib'; import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { Construct } from 'constructs'; +import { CustomAttributeConfig } from 'aws-cdk-lib/aws-cognito'; +import { ICustomAttribute } from 'aws-cdk-lib/aws-cognito'; +import { NumberAttributeConstraints } from 'aws-cdk-lib/aws-cognito'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { SecretValue } from 'aws-cdk-lib'; import { StandardAttributes } from 'aws-cdk-lib/aws-cognito'; +import { StringAttributeConstraints } from 'aws-cdk-lib/aws-cognito'; import { UserPoolIdentityProviderSamlMetadata } from 'aws-cdk-lib/aws-cognito'; // @public @@ -20,6 +24,8 @@ export type AmazonProviderProps = Omit { constructor(scope: Construct, id: string, props?: AuthProps); + // Warning: (ae-forgotten-export) The symbol "CustomAttributes" needs to be exported by the entry point index.d.ts + bindCustomAttribute: (key: string, attribute: CustomAttributes) => CustomAttributeConfig & ICustomAttribute; readonly resources: AuthResources; } @@ -43,7 +49,7 @@ export type AuthProps = { phone?: PhoneNumberLogin; externalProviders?: ExternalProviderOptions; }; - userAttributes?: StandardAttributes; + userAttributes?: UserAttributes; multifactor?: MFA; accountRecovery?: keyof typeof aws_cognito.AccountRecovery; groups?: string[]; @@ -136,6 +142,9 @@ export type TriggerEvent = (typeof triggerEvents)[number]; // @public export const triggerEvents: readonly ["createAuthChallenge", "customMessage", "defineAuthChallenge", "postAuthentication", "postConfirmation", "preAuthentication", "preSignUp", "preTokenGeneration", "userMigration", "verifyAuthChallengeResponse"]; +// @public +export type UserAttributes = StandardAttributes & Record<`custom:${string}`, CustomAttributes>; + // @public (undocumented) export type VerificationEmailWithCode = { verificationEmailStyle?: 'CODE'; diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index ea9f573e2c9..7c5f53cf644 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -19,6 +19,7 @@ export { TriggerEvent, IdentityProviderProps, AttributeMapping, + UserAttributes, } from './types.js'; export { AmplifyAuth } from './construct.js'; export { triggerEvents } from './trigger_events.js'; From d727d150bb39f4e04a43d105d2864627b796766d Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 12:13:55 -0700 Subject: [PATCH 06/31] export CustomAttributes in index.ts --- packages/auth-construct/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index 7c5f53cf644..597f7f57fe3 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -20,6 +20,7 @@ export { IdentityProviderProps, AttributeMapping, UserAttributes, + CustomAttributes, } from './types.js'; export { AmplifyAuth } from './construct.js'; export { triggerEvents } from './trigger_events.js'; From 9c30b9f1457e53cfb19b82b0c3dda7321335d03d Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 12:16:47 -0700 Subject: [PATCH 07/31] update API.MD --- packages/auth-construct/API.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index 8bf044b690f..f69628dbf45 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -24,7 +24,6 @@ export type AmazonProviderProps = Omit { constructor(scope: Construct, id: string, props?: AuthProps); - // Warning: (ae-forgotten-export) The symbol "CustomAttributes" needs to be exported by the entry point index.d.ts bindCustomAttribute: (key: string, attribute: CustomAttributes) => CustomAttributeConfig & ICustomAttribute; readonly resources: AuthResources; } @@ -56,6 +55,14 @@ export type AuthProps = { outputStorageStrategy?: BackendOutputStorageStrategy; }; +// Warning: (ae-forgotten-export) The symbol "CustomAttributeString" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "CustomAttributeNumber" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "CustomAttributeBoolean" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "CustomAttributeDateTime" needs to be exported by the entry point index.d.ts +// +// @public +export type CustomAttributes = CustomAttributeString | CustomAttributeNumber | CustomAttributeBoolean | CustomAttributeDateTime; + // @public export type EmailLogin = true | EmailLoginSettings; From 8c69e22515998ba8648031ca7f4291c336a2cc65 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 12:20:04 -0700 Subject: [PATCH 08/31] export CustomAttributesString --- packages/auth-construct/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index 597f7f57fe3..825acf1f2a5 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -21,6 +21,10 @@ export { AttributeMapping, UserAttributes, CustomAttributes, + CustomAttributeString, + CustomAttributeNumber, + CustomAttributeBoolean, + CustomAttributeDateTime, } from './types.js'; export { AmplifyAuth } from './construct.js'; export { triggerEvents } from './trigger_events.js'; From 1ae3aca5e8456e96c76503682c25c445564d1898 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 12:38:04 -0700 Subject: [PATCH 09/31] update API.md --- packages/auth-construct/API.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index f69628dbf45..a734750beaf 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -55,14 +55,31 @@ export type AuthProps = { outputStorageStrategy?: BackendOutputStorageStrategy; }; -// Warning: (ae-forgotten-export) The symbol "CustomAttributeString" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "CustomAttributeNumber" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "CustomAttributeBoolean" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "CustomAttributeDateTime" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "CustomAttributeBase" needs to be exported by the entry point index.d.ts // +// @public +export type CustomAttributeBoolean = CustomAttributeBase & { + dataType: 'Boolean'; +}; + +// @public +export type CustomAttributeDateTime = CustomAttributeBase & { + dataType: 'DateTime'; +}; + +// @public +export type CustomAttributeNumber = CustomAttributeBase & NumberAttributeConstraints & { + dataType: 'Number'; +}; + // @public export type CustomAttributes = CustomAttributeString | CustomAttributeNumber | CustomAttributeBoolean | CustomAttributeDateTime; +// @public +export type CustomAttributeString = CustomAttributeBase & StringAttributeConstraints & { + dataType: 'String'; +}; + // @public export type EmailLogin = true | EmailLoginSettings; From 4bf657c5b1248681e1e1c17a8de8a12f412ac6a2 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 12:46:02 -0700 Subject: [PATCH 10/31] export CustomAttributeBase in index.ts --- packages/auth-construct/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index 825acf1f2a5..05bef133c04 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -25,6 +25,7 @@ export { CustomAttributeNumber, CustomAttributeBoolean, CustomAttributeDateTime, + CustomAttributeBase, } from './types.js'; export { AmplifyAuth } from './construct.js'; export { triggerEvents } from './trigger_events.js'; From 6a8621a813a9310d3ea14408ac0b1105bcfe8896 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 12:49:04 -0700 Subject: [PATCH 11/31] update API.md --- packages/auth-construct/API.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index a734750beaf..a5eb0517e45 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -55,8 +55,11 @@ export type AuthProps = { outputStorageStrategy?: BackendOutputStorageStrategy; }; -// Warning: (ae-forgotten-export) The symbol "CustomAttributeBase" needs to be exported by the entry point index.d.ts -// +// @public +export type CustomAttributeBase = { + mutable?: boolean; +}; + // @public export type CustomAttributeBoolean = CustomAttributeBase & { dataType: 'Boolean'; From 5bd714ef25022cd539453af8fa5856f69bdf0d00 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 16:13:48 -0700 Subject: [PATCH 12/31] updated CustomAttribute type name --- packages/auth-construct/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index d54ecb791ed..e02f915c2ca 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -367,7 +367,7 @@ export type CustomAttributeDateTime = CustomAttributeBase & { /** * CustomAttributes is a union type that represents all the different types of custom attributes. */ -export type CustomAttributes = +export type CustomAttribute = | CustomAttributeString | CustomAttributeNumber | CustomAttributeBoolean @@ -377,7 +377,7 @@ export type CustomAttributes = * standard attributes and any number of custom attributes defined with a 'custom:' prefix. */ export type UserAttributes = StandardAttributes & - Record<`custom:${string}`, CustomAttributes>; + Record<`custom:${string}`, CustomAttribute>; /** * Input props for the AmplifyAuth construct From b2419642ec97dc33bfbb1d0242c3f131d226084c Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 16:17:12 -0700 Subject: [PATCH 13/31] updated bindbindCustomAttribute method --- packages/auth-construct/src/construct.ts | 64 ++++++++++++------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 7256070dc5e..0908b7fa0f3 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -41,7 +41,7 @@ import { AuthOutput, authOutputKey } from '@aws-amplify/backend-output-schemas'; import { AttributeMapping, AuthProps, - CustomAttributes, + CustomAttribute, EmailLoginSettings, ExternalProviderOptions, } from './types.js'; @@ -218,37 +218,6 @@ export class AmplifyAuth ); } - /** - * Define bindCustomAttribute to meet requirements of the Cognito API to call the bind method - */ - public bindCustomAttribute = ( - key: string, - attribute: CustomAttributes - ): CustomAttributeConfig & ICustomAttribute => { - const config: CustomAttributeConfig = { - dataType: attribute.dataType, - mutable: attribute.mutable ?? true, - stringConstraints: - attribute.dataType === 'String' - ? { - minLen: attribute.minLen, - maxLen: attribute.maxLen, - } - : undefined, - numberConstraints: - attribute.dataType === 'Number' - ? { - min: attribute.min, - max: attribute.max, - } - : undefined, - }; - return { - ...config, - bind: () => config, - }; - }; - /** * Create Auth/UnAuth Roles * @returns DefaultRoles @@ -382,6 +351,37 @@ export class AmplifyAuth }; }; + /** + * Define bindCustomAttribute to meet requirements of the Cognito API to call the bind method + */ + private bindCustomAttribute = ( + key: string, + attribute: CustomAttribute + ): CustomAttributeConfig & ICustomAttribute => { + const config: CustomAttributeConfig = { + dataType: attribute.dataType, + mutable: attribute.mutable ?? true, + stringConstraints: + attribute.dataType === 'String' + ? { + minLen: attribute.minLen, + maxLen: attribute.maxLen, + } + : undefined, + numberConstraints: + attribute.dataType === 'Number' + ? { + min: attribute.min, + max: attribute.max, + } + : undefined, + }; + return { + ...config, + bind: () => config, + }; + }; + /** * Process props into UserPoolProps (set defaults if needed) */ From a09c5183ceb24377b5590da268def94292fccc1e Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 16:47:41 -0700 Subject: [PATCH 14/31] removed bindCustomAttributes unit test and updated creates user attributes unit test --- packages/auth-construct/src/construct.test.ts | 132 ++++-------------- 1 file changed, 24 insertions(+), 108 deletions(-) diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index 80675751b10..c261a0d6d63 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -7,15 +7,9 @@ import { BackendOutputEntry, BackendOutputStorageStrategy, } from '@aws-amplify/plugin-types'; -import { - CfnUserPoolClient, - CustomAttributeConfig, - ICustomAttribute, - ProviderAttribute, -} from 'aws-cdk-lib/aws-cognito'; +import { CfnUserPoolClient, ProviderAttribute } from 'aws-cdk-lib/aws-cognito'; import { authOutputKey } from '@aws-amplify/backend-output-schemas'; import { DEFAULTS } from './defaults.js'; -import { CustomAttributes } from './types'; const googleClientId = 'googleClientId'; const googleClientSecret = 'googleClientSecret'; @@ -619,6 +613,15 @@ void describe('Auth construct', () => { max: 66, min: 1, }, + 'custom:member_year': { + dataType: 'Number', + max: 90, + min: 0, + }, + 'custom:favorite_song': { + dataType: 'String', + mutable: true, + }, }, }); const template = Template.fromStack(stack); @@ -657,6 +660,20 @@ void describe('Auth construct', () => { MinValue: '1', }, }, + { + AttributeDataType: 'Number', + Name: 'member_year', + Mutable: true, + NumberAttributeConstraints: { + MaxValue: '90', + MinValue: '0', + }, + }, + { + AttributeDataType: 'String', + Name: 'favorite_song', + Mutable: true, + }, ], }); }); @@ -2608,105 +2625,4 @@ void describe('Auth construct', () => { assert.equal(name.startsWith(expectedPrefix), true); }); }); - - void describe('bindCustomAttribute', () => { - let auth: AmplifyAuth; - void beforeEach(() => { - const app = new App(); - const stack = new Stack(app); - auth = new AmplifyAuth(stack, 'test'); - }); - - void it('should bind string attribute correctly', () => { - const key = 'test_string'; - const attribute: CustomAttributes = { - dataType: 'String', - mutable: true, - minLen: 3, - maxLen: 10, - }; - const expected: Omit = { - dataType: 'String', - mutable: true, - stringConstraints: { - minLen: 3, - maxLen: 10, - }, - numberConstraints: undefined, - }; - const result = auth.bindCustomAttribute(key, attribute); - assert.deepEqual(result, { ...expected, bind: result.bind }); - // Test bind function separately - const bindResult = result.bind(); - assert.deepEqual(bindResult, expected); - }); - - void it('should bind number attribute correctly', () => { - const key = 'test_number'; - const attribute: CustomAttributes = { - dataType: 'Number', - mutable: false, - min: 1, - max: 100, - }; - const expected: Omit = { - dataType: 'Number', - mutable: false, - stringConstraints: undefined, - numberConstraints: { - min: 1, - max: 100, - }, - }; - const result = auth.bindCustomAttribute(key, attribute); - assert.deepEqual(result, { ...expected, bind: result.bind }); - // Test bind function separately - const bindResult = result.bind(); - assert.deepEqual(bindResult, expected); - }); - - void it('should handle missing constraints correctly', () => { - const key = 'test_string_no_constraints'; - const attribute: CustomAttributes = { - dataType: 'String', - mutable: true, - }; - const expected: Omit = { - dataType: 'String', - mutable: true, - stringConstraints: { - minLen: undefined, - maxLen: undefined, - }, - numberConstraints: undefined, - }; - const result = auth.bindCustomAttribute(key, attribute); - assert.deepEqual(result, { ...expected, bind: result.bind }); - // Test bind function separately - const bindResult = result.bind(); - assert.deepEqual(bindResult, expected); - }); - - void it('should set mutable to true when not defined', () => { - const key = 'test_string_no_mutable'; - const attribute: CustomAttributes = { - dataType: 'String', - }; - const expected: Omit = { - dataType: 'String', - mutable: true, - stringConstraints: { - minLen: undefined, - maxLen: undefined, - }, - numberConstraints: undefined, - }; - const result = auth.bindCustomAttribute(key, attribute); - assert.deepEqual(result, { ...expected, bind: result.bind }); - - // Test bind function separately - const bindResult = result.bind(); - assert.deepEqual(bindResult, expected); - }); - }); }); From 3b7cf75213e649327839e593034d6975b00fe288 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 16:48:55 -0700 Subject: [PATCH 15/31] updated index export --- packages/auth-construct/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index 05bef133c04..13af450f20c 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -20,7 +20,7 @@ export { IdentityProviderProps, AttributeMapping, UserAttributes, - CustomAttributes, + CustomAttribute, CustomAttributeString, CustomAttributeNumber, CustomAttributeBoolean, From 35503b825e8e67f94bd667fa9ff8b662840b184d Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Thu, 11 Jul 2024 16:53:57 -0700 Subject: [PATCH 16/31] updated API.md --- packages/auth-construct/API.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index a5eb0517e45..7f0fa767883 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -9,8 +9,6 @@ import { AuthResources } from '@aws-amplify/plugin-types'; import { aws_cognito } from 'aws-cdk-lib'; import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { Construct } from 'constructs'; -import { CustomAttributeConfig } from 'aws-cdk-lib/aws-cognito'; -import { ICustomAttribute } from 'aws-cdk-lib/aws-cognito'; import { NumberAttributeConstraints } from 'aws-cdk-lib/aws-cognito'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { SecretValue } from 'aws-cdk-lib'; @@ -24,7 +22,6 @@ export type AmazonProviderProps = Omit { constructor(scope: Construct, id: string, props?: AuthProps); - bindCustomAttribute: (key: string, attribute: CustomAttributes) => CustomAttributeConfig & ICustomAttribute; readonly resources: AuthResources; } @@ -55,6 +52,9 @@ export type AuthProps = { outputStorageStrategy?: BackendOutputStorageStrategy; }; +// @public +export type CustomAttribute = CustomAttributeString | CustomAttributeNumber | CustomAttributeBoolean | CustomAttributeDateTime; + // @public export type CustomAttributeBase = { mutable?: boolean; @@ -75,9 +75,6 @@ export type CustomAttributeNumber = CustomAttributeBase & NumberAttributeConstra dataType: 'Number'; }; -// @public -export type CustomAttributes = CustomAttributeString | CustomAttributeNumber | CustomAttributeBoolean | CustomAttributeDateTime; - // @public export type CustomAttributeString = CustomAttributeBase & StringAttributeConstraints & { dataType: 'String'; @@ -170,7 +167,7 @@ export type TriggerEvent = (typeof triggerEvents)[number]; export const triggerEvents: readonly ["createAuthChallenge", "customMessage", "defineAuthChallenge", "postAuthentication", "postConfirmation", "preAuthentication", "preSignUp", "preTokenGeneration", "userMigration", "verifyAuthChallengeResponse"]; // @public -export type UserAttributes = StandardAttributes & Record<`custom:${string}`, CustomAttributes>; +export type UserAttributes = StandardAttributes & Record<`custom:${string}`, CustomAttribute>; // @public (undocumented) export type VerificationEmailWithCode = { From 4de16d7fcd3bb3b1ebdb5a77bd6c99070c0cb046 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Fri, 12 Jul 2024 00:16:42 -0700 Subject: [PATCH 17/31] updated bindCustomAttribute method --- packages/auth-construct/src/construct.ts | 50 ++++++++++++++++-------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 0908b7fa0f3..776ef0ae126 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -358,30 +358,48 @@ export class AmplifyAuth key: string, attribute: CustomAttribute ): CustomAttributeConfig & ICustomAttribute => { - const config: CustomAttributeConfig = { + const baseConfig: CustomAttributeConfig = { dataType: attribute.dataType, + mutable: attribute.mutable ?? true, - stringConstraints: - attribute.dataType === 'String' - ? { - minLen: attribute.minLen, - maxLen: attribute.maxLen, - } - : undefined, - numberConstraints: - attribute.dataType === 'Number' - ? { - min: attribute.min, - max: attribute.max, - } - : undefined, }; + + let constraints = {}; + // Conditionally add constraint properties based on dataType. + if (attribute.dataType === 'String') { + constraints = { + ...constraints, + + stringConstraints: { + minLen: attribute.minLen, + + maxLen: attribute.maxLen, + }, + }; + } else if (attribute.dataType === 'Number') { + constraints = { + ...constraints, + + numberConstraints: { + min: attribute.min, + + max: attribute.max, + }, + }; + } + //The final config object includes baseConfig and conditionally added constraint properties. + const config = { + ...baseConfig, + + ...constraints, + }; + return { ...config, + bind: () => config, }; }; - /** * Process props into UserPoolProps (set defaults if needed) */ From e00f6a4737704c5ad77860fa26f1273d049eb0f7 Mon Sep 17 00:00:00 2001 From: fangyu wu <98557748+fangyuwu7@users.noreply.github.com> Date: Fri, 12 Jul 2024 00:34:49 -0700 Subject: [PATCH 18/31] Update blue-turkeys-trade.md --- .changeset/blue-turkeys-trade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/blue-turkeys-trade.md b/.changeset/blue-turkeys-trade.md index 5d3bf27bde1..1b520f6623b 100644 --- a/.changeset/blue-turkeys-trade.md +++ b/.changeset/blue-turkeys-trade.md @@ -2,4 +2,4 @@ '@aws-amplify/auth-construct': minor --- -adding customAttributes into userAttributes +Added customAttributes into userAttributes From 458be4e6b53560309fed9cca32add37d20752884 Mon Sep 17 00:00:00 2001 From: fangyu wu <98557748+fangyuwu7@users.noreply.github.com> Date: Fri, 12 Jul 2024 00:50:14 -0700 Subject: [PATCH 19/31] Update blue-turkeys-trade.md --- .changeset/blue-turkeys-trade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/blue-turkeys-trade.md b/.changeset/blue-turkeys-trade.md index 1b520f6623b..49e89ccabf6 100644 --- a/.changeset/blue-turkeys-trade.md +++ b/.changeset/blue-turkeys-trade.md @@ -2,4 +2,4 @@ '@aws-amplify/auth-construct': minor --- -Added customAttributes into userAttributes +Add customAttributes into userAttributes From de683c1a8ab1065d003b8e7f569171e689da4f17 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Fri, 12 Jul 2024 01:22:19 -0700 Subject: [PATCH 20/31] update type CustomAttributeBoolean --- packages/auth-construct/src/index.ts | 2 +- packages/auth-construct/src/types.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index 13af450f20c..6845478123d 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -23,7 +23,7 @@ export { CustomAttribute, CustomAttributeString, CustomAttributeNumber, - CustomAttributeBoolean, + CustomAttributesBoolean, CustomAttributeDateTime, CustomAttributeBase, } from './types.js'; diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index e02f915c2ca..ffe1b65d492 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -355,7 +355,7 @@ export type CustomAttributeNumber = CustomAttributeBase & /** * CustomAttributeBoolean represents a custom attribute of type boolean. */ -export type CustomAttributeBoolean = CustomAttributeBase & { +export type CustomAttributesBoolean = CustomAttributeBase & { dataType: 'Boolean'; }; /** @@ -370,7 +370,7 @@ export type CustomAttributeDateTime = CustomAttributeBase & { export type CustomAttribute = | CustomAttributeString | CustomAttributeNumber - | CustomAttributeBoolean + | CustomAttributesBoolean | CustomAttributeDateTime; /** * UserAttributes represents the combined attributes of a user, including From f728168d80f584b71d8073a06aa1f78a755b557c Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Fri, 12 Jul 2024 01:27:43 -0700 Subject: [PATCH 21/31] update /API.md --- packages/auth-construct/API.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index 7f0fa767883..be8fe0b32a6 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -53,18 +53,13 @@ export type AuthProps = { }; // @public -export type CustomAttribute = CustomAttributeString | CustomAttributeNumber | CustomAttributeBoolean | CustomAttributeDateTime; +export type CustomAttribute = CustomAttributeString | CustomAttributeNumber | CustomAttributesBoolean | CustomAttributeDateTime; // @public export type CustomAttributeBase = { mutable?: boolean; }; -// @public -export type CustomAttributeBoolean = CustomAttributeBase & { - dataType: 'Boolean'; -}; - // @public export type CustomAttributeDateTime = CustomAttributeBase & { dataType: 'DateTime'; @@ -75,6 +70,11 @@ export type CustomAttributeNumber = CustomAttributeBase & NumberAttributeConstra dataType: 'Number'; }; +// @public +export type CustomAttributesBoolean = CustomAttributeBase & { + dataType: 'Boolean'; +}; + // @public export type CustomAttributeString = CustomAttributeBase & StringAttributeConstraints & { dataType: 'String'; From 74f39521a49a402effdc1fe7e23bfb14443f1f6d Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Fri, 12 Jul 2024 01:52:51 -0700 Subject: [PATCH 22/31] update type CustomAttributesBoolean --- packages/auth-construct/src/index.ts | 2 +- packages/auth-construct/src/types.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index 6845478123d..13af450f20c 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -23,7 +23,7 @@ export { CustomAttribute, CustomAttributeString, CustomAttributeNumber, - CustomAttributesBoolean, + CustomAttributeBoolean, CustomAttributeDateTime, CustomAttributeBase, } from './types.js'; diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index ffe1b65d492..e02f915c2ca 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -355,7 +355,7 @@ export type CustomAttributeNumber = CustomAttributeBase & /** * CustomAttributeBoolean represents a custom attribute of type boolean. */ -export type CustomAttributesBoolean = CustomAttributeBase & { +export type CustomAttributeBoolean = CustomAttributeBase & { dataType: 'Boolean'; }; /** @@ -370,7 +370,7 @@ export type CustomAttributeDateTime = CustomAttributeBase & { export type CustomAttribute = | CustomAttributeString | CustomAttributeNumber - | CustomAttributesBoolean + | CustomAttributeBoolean | CustomAttributeDateTime; /** * UserAttributes represents the combined attributes of a user, including From 2e871d875d1537ee7cef6866fc7cebb502899556 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Fri, 12 Jul 2024 01:55:45 -0700 Subject: [PATCH 23/31] update API.md --- packages/auth-construct/API.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index be8fe0b32a6..7f0fa767883 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -53,13 +53,18 @@ export type AuthProps = { }; // @public -export type CustomAttribute = CustomAttributeString | CustomAttributeNumber | CustomAttributesBoolean | CustomAttributeDateTime; +export type CustomAttribute = CustomAttributeString | CustomAttributeNumber | CustomAttributeBoolean | CustomAttributeDateTime; // @public export type CustomAttributeBase = { mutable?: boolean; }; +// @public +export type CustomAttributeBoolean = CustomAttributeBase & { + dataType: 'Boolean'; +}; + // @public export type CustomAttributeDateTime = CustomAttributeBase & { dataType: 'DateTime'; @@ -70,11 +75,6 @@ export type CustomAttributeNumber = CustomAttributeBase & NumberAttributeConstra dataType: 'Number'; }; -// @public -export type CustomAttributesBoolean = CustomAttributeBase & { - dataType: 'Boolean'; -}; - // @public export type CustomAttributeString = CustomAttributeBase & StringAttributeConstraints & { dataType: 'String'; From afd8f9190a85bc17e7d0f7a0542c6ef4316e278d Mon Sep 17 00:00:00 2001 From: Amplifiyer <51211245+Amplifiyer@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:56:52 +0200 Subject: [PATCH 24/31] refactor top level cli error handling (#1730) * refactor top level cli error handling * PR updates * Add cause * update api after clean build --- .changeset/empty-cameras-smile.md | 7 +++ packages/cli/src/ampx.ts | 21 ++++---- packages/cli/src/main_parser_factory.test.ts | 32 +++++++++--- packages/cli/src/main_parser_factory.ts | 12 ++--- packages/cli/src/parse_async_safely.test.ts | 16 ------ packages/cli/src/parse_async_safely.ts | 23 --------- packages/platform-core/API.md | 2 +- .../platform-core/src/errors/amplify_error.ts | 14 ++++- .../src/usage-data/account_id_fetcher.test.ts | 51 +++++++++++++++++++ .../src/usage-data/account_id_fetcher.ts | 26 +++++++--- .../src/usage-data/usage_data_emitter.ts | 36 ++++++++----- .../sandbox/src/file_watching_sandbox.test.ts | 32 +++++++++++- packages/sandbox/src/file_watching_sandbox.ts | 23 ++++++++- 13 files changed, 203 insertions(+), 92 deletions(-) create mode 100644 .changeset/empty-cameras-smile.md delete mode 100644 packages/cli/src/parse_async_safely.test.ts delete mode 100644 packages/cli/src/parse_async_safely.ts create mode 100644 packages/platform-core/src/usage-data/account_id_fetcher.test.ts diff --git a/.changeset/empty-cameras-smile.md b/.changeset/empty-cameras-smile.md new file mode 100644 index 00000000000..a53a3dc1b0e --- /dev/null +++ b/.changeset/empty-cameras-smile.md @@ -0,0 +1,7 @@ +--- +'@aws-amplify/platform-core': patch +'@aws-amplify/sandbox': patch +'@aws-amplify/backend-cli': patch +--- + +refactor top level cli error handling diff --git a/packages/cli/src/ampx.ts b/packages/cli/src/ampx.ts index 0d31d3c92c6..2ddb90ccb26 100755 --- a/packages/cli/src/ampx.ts +++ b/packages/cli/src/ampx.ts @@ -1,6 +1,9 @@ #!/usr/bin/env node import { createMainParser } from './main_parser_factory.js'; -import { attachUnhandledExceptionListeners } from './error_handler.js'; +import { + attachUnhandledExceptionListeners, + generateCommandFailureHandler, +} from './error_handler.js'; import { extractSubCommands } from './extract_sub_commands.js'; import { AmplifyFault, @@ -8,9 +11,9 @@ import { UsageDataEmitterFactory, } from '@aws-amplify/platform-core'; import { fileURLToPath } from 'node:url'; -import { LogLevel, format, printer } from '@aws-amplify/cli-core'; import { verifyCommandName } from './verify_command_name.js'; -import { parseAsyncSafely } from './parse_async_safely.js'; +import { hideBin } from 'yargs/helpers'; +import { format } from '@aws-amplify/cli-core'; const packageJson = new PackageJsonReader().read( fileURLToPath(new URL('../package.json', import.meta.url)) @@ -32,11 +35,11 @@ attachUnhandledExceptionListeners(usageDataEmitter); verifyCommandName(); -const parser = createMainParser(libraryVersion, usageDataEmitter); - -await parseAsyncSafely(parser); +const parser = createMainParser(libraryVersion); +const errorHandler = generateCommandFailureHandler(parser, usageDataEmitter); try { + await parser.parseAsync(hideBin(process.argv)); const metricDimension: Record = {}; const subCommands = extractSubCommands(parser); @@ -47,10 +50,6 @@ try { await usageDataEmitter.emitSuccess({}, metricDimension); } catch (e) { if (e instanceof Error) { - printer.log(format.error('Failed to emit usage metrics'), LogLevel.DEBUG); - printer.log(format.error(e), LogLevel.DEBUG); - if (e.stack) { - printer.log(e.stack, LogLevel.DEBUG); - } + await errorHandler(format.error(e), e); } } diff --git a/packages/cli/src/main_parser_factory.test.ts b/packages/cli/src/main_parser_factory.test.ts index aefec2e5632..5a54ffa4a52 100644 --- a/packages/cli/src/main_parser_factory.test.ts +++ b/packages/cli/src/main_parser_factory.test.ts @@ -1,6 +1,9 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { TestCommandRunner } from './test-utils/command_runner.js'; +import { + TestCommandError, + TestCommandRunner, +} from './test-utils/command_runner.js'; import { createMainParser } from './main_parser_factory.js'; import { version } from '#package.json'; @@ -20,16 +23,29 @@ void describe('main parser', { concurrency: false }, () => { }); void it('prints help if command is not provided', async () => { - const output = await commandRunner.runCommand(''); - assert.match(output, /Commands:/); - assert.match(output, /Not enough non-option arguments:/); + await assert.rejects( + () => commandRunner.runCommand(''), + (err) => { + assert(err instanceof TestCommandError); + assert.match(err.output, /Commands:/); + assert.match(err.error.message, /Not enough non-option arguments:/); + return true; + } + ); }); void it('errors and prints help if invalid option is given', async () => { - const output = await commandRunner.runCommand( - 'sandbox --non-existing-option 1' + await assert.rejects( + () => commandRunner.runCommand('sandbox --non-existing-option 1'), + (err) => { + assert(err instanceof TestCommandError); + assert.match(err.output, /Commands:/); + assert.match( + err.error.message, + /Unknown arguments: non-existing-option/ + ); + return true; + } ); - assert.match(output, /Commands:/); - assert.match(output, /Unknown arguments: non-existing-option/); }); }); diff --git a/packages/cli/src/main_parser_factory.ts b/packages/cli/src/main_parser_factory.ts index 0747c22e8eb..5189c7de3f2 100644 --- a/packages/cli/src/main_parser_factory.ts +++ b/packages/cli/src/main_parser_factory.ts @@ -1,20 +1,15 @@ import yargs, { Argv } from 'yargs'; -import { UsageDataEmitter } from '@aws-amplify/platform-core'; import { createGenerateCommand } from './commands/generate/generate_command_factory.js'; import { createSandboxCommand } from './commands/sandbox/sandbox_command_factory.js'; import { createPipelineDeployCommand } from './commands/pipeline-deploy/pipeline_deploy_command_factory.js'; import { createConfigureCommand } from './commands/configure/configure_command_factory.js'; -import { generateCommandFailureHandler } from './error_handler.js'; import { createInfoCommand } from './commands/info/info_command_factory.js'; import * as path from 'path'; /** * Creates main parser. */ -export const createMainParser = ( - libraryVersion: string, - usageDataEmitter?: UsageDataEmitter -): Argv => { +export const createMainParser = (libraryVersion: string): Argv => { const parser = yargs() .version(libraryVersion) // This option is being used indirectly to configure the log level of the Printer instance. @@ -36,9 +31,8 @@ export const createMainParser = ( .help() .demandCommand() .strictCommands() - .recommendCommands(); - - parser.fail(generateCommandFailureHandler(parser, usageDataEmitter)); + .recommendCommands() + .fail(false); return parser; }; diff --git a/packages/cli/src/parse_async_safely.test.ts b/packages/cli/src/parse_async_safely.test.ts deleted file mode 100644 index 24f3327f158..00000000000 --- a/packages/cli/src/parse_async_safely.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Argv } from 'yargs'; -import { describe, it } from 'node:test'; -import { parseAsyncSafely } from './parse_async_safely.js'; - -const mockParser: Argv = { - parseAsync: () => { - throw new Error('Mock parser error'); - }, -} as unknown as Argv; - -void describe('execute parseAsyncSafely', () => { - void it('parseAsyncSafely should not throw an error', async () => { - await parseAsyncSafely(mockParser); - //no throw - }); -}); diff --git a/packages/cli/src/parse_async_safely.ts b/packages/cli/src/parse_async_safely.ts deleted file mode 100644 index d6f229c096f..00000000000 --- a/packages/cli/src/parse_async_safely.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Argv } from 'yargs'; -import { LogLevel, format, printer } from '@aws-amplify/cli-core'; -import { hideBin } from 'yargs/helpers'; -/** - Executes the command using the provided parser. - Handles any errors that occur during command execution. - @param parser - The parser object to parse the command line arguments. - @returns - A promise that resolves when the command execution is complete. - */ -export const parseAsyncSafely = async (parser: Argv): Promise => { - try { - await parser.parseAsync(hideBin(process.argv)); - // Yargs invoke the command failure handler before rethrowing the error.This prevents it from propagating to unhandled exception handler and being printed again. - } catch (e) { - if (e instanceof Error) { - printer.log(format.error('Failed to execute command'), LogLevel.DEBUG); - printer.log(format.error(e), LogLevel.DEBUG); - if (e.stack) { - printer.log(e.stack, LogLevel.DEBUG); - } - } - } -}; diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index d257509bf4b..79cad2c92fd 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -21,7 +21,7 @@ export abstract class AmplifyError extends Error { // (undocumented) readonly details?: string; // (undocumented) - static fromError: (error: unknown) => AmplifyError<'UnknownFault'>; + static fromError: (error: unknown) => AmplifyError<'UnknownFault' | 'CredentialsError'>; // (undocumented) static fromStderr: (_stderr: string) => AmplifyError | undefined; // (undocumented) diff --git a/packages/platform-core/src/errors/amplify_error.ts b/packages/platform-core/src/errors/amplify_error.ts index 4fbfbe34160..1634bcdbf2a 100644 --- a/packages/platform-core/src/errors/amplify_error.ts +++ b/packages/platform-core/src/errors/amplify_error.ts @@ -98,12 +98,20 @@ export abstract class AmplifyError extends Error { return undefined; }; - static fromError = (error: unknown): AmplifyError<'UnknownFault'> => { + static fromError = ( + error: unknown + ): AmplifyError<'UnknownFault' | 'CredentialsError'> => { const errorMessage = error instanceof Error ? `${error.name}: ${error.message}` : 'An unknown error happened. Check downstream error'; + if (error instanceof Error && isCredentialsError(error)) { + return new AmplifyUserError('CredentialsError', { + message: errorMessage, + resolution: '', + }); + } return new AmplifyFault( 'UnknownFault', { @@ -114,6 +122,10 @@ export abstract class AmplifyError extends Error { }; } +const isCredentialsError = (err?: Error): boolean => { + return !!err && err?.name === 'CredentialsProviderError'; +}; + /** * Amplify exception classifications */ diff --git a/packages/platform-core/src/usage-data/account_id_fetcher.test.ts b/packages/platform-core/src/usage-data/account_id_fetcher.test.ts new file mode 100644 index 00000000000..e12caa8d0ec --- /dev/null +++ b/packages/platform-core/src/usage-data/account_id_fetcher.test.ts @@ -0,0 +1,51 @@ +import { AccountIdFetcher } from './account_id_fetcher'; +import { GetCallerIdentityCommandOutput, STSClient } from '@aws-sdk/client-sts'; +import { describe, mock, test } from 'node:test'; +import assert from 'node:assert'; + +void describe('AccountIdFetcher', async () => { + void test('fetches account ID successfully', async () => { + const mockSend = mock.method(STSClient.prototype, 'send', () => + Promise.resolve({ + Account: '123456789012', + } as GetCallerIdentityCommandOutput) + ); + + const accountIdFetcher = new AccountIdFetcher(new STSClient({})); + const accountId = await accountIdFetcher.fetch(); + + assert.strictEqual(accountId, '123456789012'); + mockSend.mock.resetCalls(); + }); + + void test('returns default account ID when STS fails', async () => { + const mockSend = mock.method(STSClient.prototype, 'send', () => + Promise.reject(new Error('STS error')) + ); + + const accountIdFetcher = new AccountIdFetcher(new STSClient({})); + const accountId = await accountIdFetcher.fetch(); + + assert.strictEqual(accountId, 'NO_ACCOUNT_ID'); + mockSend.mock.resetCalls(); + }); + + void test('returns cached account ID on subsequent calls', async () => { + const mockSend = mock.method(STSClient.prototype, 'send', () => + Promise.resolve({ + Account: '123456789012', + } as GetCallerIdentityCommandOutput) + ); + + const accountIdFetcher = new AccountIdFetcher(new STSClient({})); + const accountId1 = await accountIdFetcher.fetch(); + const accountId2 = await accountIdFetcher.fetch(); + + assert.strictEqual(accountId1, '123456789012'); + assert.strictEqual(accountId2, '123456789012'); + + // we only call the service once. + assert.strictEqual(mockSend.mock.callCount(), 1); + mockSend.mock.resetCalls(); + }); +}); diff --git a/packages/platform-core/src/usage-data/account_id_fetcher.ts b/packages/platform-core/src/usage-data/account_id_fetcher.ts index 102dcdf605c..f430f9e6722 100644 --- a/packages/platform-core/src/usage-data/account_id_fetcher.ts +++ b/packages/platform-core/src/usage-data/account_id_fetcher.ts @@ -1,22 +1,32 @@ import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; +const NO_ACCOUNT_ID = 'NO_ACCOUNT_ID'; /** * Retrieves the account ID of the user */ export class AccountIdFetcher { + private accountId: string | undefined; /** * constructor for AccountIdFetcher */ constructor(private readonly stsClient = new STSClient()) {} fetch = async () => { - const stsResponse = await this.stsClient.send( - new GetCallerIdentityCommand({}) - ); - if (stsResponse && stsResponse.Account) { - return stsResponse.Account; + if (this.accountId) { + return this.accountId; + } + try { + const stsResponse = await this.stsClient.send( + new GetCallerIdentityCommand({}) + ); + if (stsResponse && stsResponse.Account) { + this.accountId = stsResponse.Account; + return this.accountId; + } + // We failed to get the account Id. Most likely the user doesn't have credentials + return NO_ACCOUNT_ID; + } catch (error) { + // We failed to get the account Id. Most likely the user doesn't have credentials + return NO_ACCOUNT_ID; } - throw new Error( - 'Cannot retrieve the account Id from GetCallerIdentityCommand' - ); }; } diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.ts b/packages/platform-core/src/usage-data/usage_data_emitter.ts index 118ce69ecf4..d108dc9fc75 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.ts @@ -29,24 +29,34 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { metrics?: Record, dimensions?: Record ) => { - const data = await this.getUsageData({ - state: 'SUCCEEDED', - metrics, - dimensions, - }); - await this.send(data); + try { + const data = await this.getUsageData({ + state: 'SUCCEEDED', + metrics, + dimensions, + }); + await this.send(data); + // eslint-disable-next-line amplify-backend-rules/no-empty-catch + } catch { + // Don't propagate errors related to not being able to send telemetry + } }; emitFailure = async ( error: AmplifyError, dimensions?: Record ) => { - const data = await this.getUsageData({ - state: 'FAILED', - error, - dimensions, - }); - await this.send(data); + try { + const data = await this.getUsageData({ + state: 'FAILED', + error, + dimensions, + }); + await this.send(data); + // eslint-disable-next-line amplify-backend-rules/no-empty-catch + } catch { + // Don't propagate errors related to not being able to send telemetry + } }; private getUsageData = async (options: { @@ -58,7 +68,7 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { return { accountId: await this.accountIdFetcher.fetch(), sessionUuid: this.sessionUuid, - installationUuid: await getInstallationUuid(), + installationUuid: getInstallationUuid(), amplifyCliVersion: this.libraryVersion, timestamp: new Date().toISOString(), error: options.error ? new SerializableError(options.error) : undefined, diff --git a/packages/sandbox/src/file_watching_sandbox.test.ts b/packages/sandbox/src/file_watching_sandbox.test.ts index f6b1781bf90..2654336a015 100644 --- a/packages/sandbox/src/file_watching_sandbox.test.ts +++ b/packages/sandbox/src/file_watching_sandbox.test.ts @@ -27,7 +27,11 @@ import { import { fileURLToPath } from 'url'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { AmplifyUserError } from '@aws-amplify/platform-core'; -import { ParameterNotFound, SSMClient } from '@aws-sdk/client-ssm'; +import { + ParameterNotFound, + SSMClient, + SSMServiceException, +} from '@aws-sdk/client-ssm'; // Watcher mocks const unsubscribeMockFn = mock.fn(); @@ -173,6 +177,32 @@ void describe('Sandbox to check if region is bootstrapped', () => { ); }); + void it('when user does not have proper credentials throw user error', async () => { + const error = new SSMServiceException({ + name: 'UnrecognizedClientException', + $fault: 'client', + $metadata: {}, + message: 'The security token included in the request is invalid.', + }); + ssmClientSendMock.mock.mockImplementationOnce(() => { + throw error; + }); + + await assert.rejects( + () => sandboxInstance.start({}), + new AmplifyUserError( + 'SSMCredentialsError', + { + message: + 'UnrecognizedClientException: The security token included in the request is invalid.', + resolution: + 'Make sure your AWS credentials are set up correctly and have permissions to call SSM:GetParameter', + }, + error + ) + ); + }); + void it('when region has bootstrapped, but with a version lower than the minimum (6), then opens console to initiate bootstrap', async () => { ssmClientSendMock.mock.mockImplementationOnce(() => Promise.resolve({ diff --git a/packages/sandbox/src/file_watching_sandbox.ts b/packages/sandbox/src/file_watching_sandbox.ts index 46e6cb9197d..e54bc2e1236 100644 --- a/packages/sandbox/src/file_watching_sandbox.ts +++ b/packages/sandbox/src/file_watching_sandbox.ts @@ -19,6 +19,7 @@ import { GetParameterCommand, ParameterNotFound, SSMClient, + SSMServiceException, } from '@aws-sdk/client-ssm'; import { AmplifyPrompter, @@ -330,7 +331,27 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { if (e instanceof ParameterNotFound) { return false; } - // If we are unable to retrieve bootstrap version parameter due to other reasons(AccessDenied), we fail fast. + if ( + e instanceof SSMServiceException && + [ + 'UnrecognizedClientException', + 'AccessDeniedException', + 'NotAuthorized', + 'ExpiredTokenException', + ].includes(e.name) + ) { + throw new AmplifyUserError( + 'SSMCredentialsError', + { + message: `${e.name}: ${e.message}`, + resolution: + 'Make sure your AWS credentials are set up correctly and have permissions to call SSM:GetParameter', + }, + e + ); + } + + // If we are unable to retrieve bootstrap version parameter due to other reasons, we fail fast. throw e; } }; From 3ef167e531e3f1417ef5168e6cf58a1c64c36854 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Sat, 13 Jul 2024 16:37:48 -0700 Subject: [PATCH 25/31] update construct.ts --- packages/auth-construct/src/construct.test.ts | 22 +++++++++++++++++-- packages/auth-construct/src/construct.ts | 6 +---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index c261a0d6d63..c82a1e7127b 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -613,7 +613,15 @@ void describe('Auth construct', () => { max: 66, min: 1, }, - 'custom:member_year': { + 'custom:register_date': { + dataType: 'DateTime', + mutable: true, + }, + 'custom:is_member': { + dataType: 'Boolean', + mutable: false, + }, + 'custom:year_as_member': { dataType: 'Number', max: 90, min: 0, @@ -660,9 +668,19 @@ void describe('Auth construct', () => { MinValue: '1', }, }, + { + AttributeDataType: 'DateTime', + Name: 'register_date', + Mutable: true, + }, + { + AttributeDataType: 'Boolean', + Name: 'is_member', + Mutable: false, + }, { AttributeDataType: 'Number', - Name: 'member_year', + Name: 'year_as_member', Mutable: true, NumberAttributeConstraints: { MaxValue: '90', diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 776ef0ae126..9a0e85206cb 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -368,8 +368,6 @@ export class AmplifyAuth // Conditionally add constraint properties based on dataType. if (attribute.dataType === 'String') { constraints = { - ...constraints, - stringConstraints: { minLen: attribute.minLen, @@ -378,8 +376,6 @@ export class AmplifyAuth }; } else if (attribute.dataType === 'Number') { constraints = { - ...constraints, - numberConstraints: { min: attribute.min, @@ -470,7 +466,7 @@ export class AmplifyAuth [key, value] ) => { if (key.startsWith('custom:')) { - const attributeKey = key.replace(/^(custom:|User\.?)/i, ''); + const attributeKey = key.replace(/^custom:/i, ''); acc.customAttributes[attributeKey] = this.bindCustomAttribute( attributeKey, value From 54089c76f41d1f1202213013a0a23383631806ef Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Sat, 13 Jul 2024 16:55:03 -0700 Subject: [PATCH 26/31] update construct.ts --- packages/auth-construct/src/construct.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 9a0e85206cb..b013020f6c4 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -466,11 +466,7 @@ export class AmplifyAuth [key, value] ) => { if (key.startsWith('custom:')) { - const attributeKey = key.replace(/^custom:/i, ''); - acc.customAttributes[attributeKey] = this.bindCustomAttribute( - attributeKey, - value - ); + acc.customAttributes[key] = this.bindCustomAttribute(key, value); } else { acc.standardAttributes[key] = value; } From e20ed2f6e3251163242144bcb56c82a3e9542035 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Sat, 13 Jul 2024 17:13:17 -0700 Subject: [PATCH 27/31] remove prefix custom: --- packages/auth-construct/src/construct.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index b013020f6c4..9a0e85206cb 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -466,7 +466,11 @@ export class AmplifyAuth [key, value] ) => { if (key.startsWith('custom:')) { - acc.customAttributes[key] = this.bindCustomAttribute(key, value); + const attributeKey = key.replace(/^custom:/i, ''); + acc.customAttributes[attributeKey] = this.bindCustomAttribute( + attributeKey, + value + ); } else { acc.standardAttributes[key] = value; } From 8c770f187e277a681438bcc1a7a84a72721d5f49 Mon Sep 17 00:00:00 2001 From: fangyu wu <98557748+fangyuwu7@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:44:46 -0700 Subject: [PATCH 28/31] Delete .changeset/empty-cameras-smile.md --- .changeset/empty-cameras-smile.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .changeset/empty-cameras-smile.md diff --git a/.changeset/empty-cameras-smile.md b/.changeset/empty-cameras-smile.md deleted file mode 100644 index a53a3dc1b0e..00000000000 --- a/.changeset/empty-cameras-smile.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@aws-amplify/platform-core': patch -'@aws-amplify/sandbox': patch -'@aws-amplify/backend-cli': patch ---- - -refactor top level cli error handling From e8c6d1653c6b3b9ac48c670e71a0b5f559e233ca Mon Sep 17 00:00:00 2001 From: fangyu wu <98557748+fangyuwu7@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:46:04 -0700 Subject: [PATCH 29/31] Delete packages/cli/src/ampx.ts --- packages/cli/src/ampx.ts | 55 ---------------------------------------- 1 file changed, 55 deletions(-) delete mode 100755 packages/cli/src/ampx.ts diff --git a/packages/cli/src/ampx.ts b/packages/cli/src/ampx.ts deleted file mode 100755 index 2ddb90ccb26..00000000000 --- a/packages/cli/src/ampx.ts +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env node -import { createMainParser } from './main_parser_factory.js'; -import { - attachUnhandledExceptionListeners, - generateCommandFailureHandler, -} from './error_handler.js'; -import { extractSubCommands } from './extract_sub_commands.js'; -import { - AmplifyFault, - PackageJsonReader, - UsageDataEmitterFactory, -} from '@aws-amplify/platform-core'; -import { fileURLToPath } from 'node:url'; -import { verifyCommandName } from './verify_command_name.js'; -import { hideBin } from 'yargs/helpers'; -import { format } from '@aws-amplify/cli-core'; - -const packageJson = new PackageJsonReader().read( - fileURLToPath(new URL('../package.json', import.meta.url)) -); -const libraryVersion = packageJson.version; - -if (libraryVersion == undefined) { - throw new AmplifyFault('UnknownVersionFault', { - message: - 'Library version cannot be determined. Check the library installation', - }); -} - -const usageDataEmitter = await new UsageDataEmitterFactory().getInstance( - libraryVersion -); - -attachUnhandledExceptionListeners(usageDataEmitter); - -verifyCommandName(); - -const parser = createMainParser(libraryVersion); -const errorHandler = generateCommandFailureHandler(parser, usageDataEmitter); - -try { - await parser.parseAsync(hideBin(process.argv)); - const metricDimension: Record = {}; - const subCommands = extractSubCommands(parser); - - if (subCommands) { - metricDimension.command = subCommands; - } - - await usageDataEmitter.emitSuccess({}, metricDimension); -} catch (e) { - if (e instanceof Error) { - await errorHandler(format.error(e), e); - } -} From df6d008de87a3604606f1710328f10383cd0bf46 Mon Sep 17 00:00:00 2001 From: fangyu wu <98557748+fangyuwu7@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:46:18 -0700 Subject: [PATCH 30/31] Delete packages/sandbox/src/file_watching_sandbox.ts --- packages/sandbox/src/file_watching_sandbox.ts | 421 ------------------ 1 file changed, 421 deletions(-) delete mode 100644 packages/sandbox/src/file_watching_sandbox.ts diff --git a/packages/sandbox/src/file_watching_sandbox.ts b/packages/sandbox/src/file_watching_sandbox.ts deleted file mode 100644 index e54bc2e1236..00000000000 --- a/packages/sandbox/src/file_watching_sandbox.ts +++ /dev/null @@ -1,421 +0,0 @@ -import debounce from 'debounce-promise'; -import parcelWatcher, { subscribe } from '@parcel/watcher'; -import { AmplifySandboxExecutor } from './sandbox_executor.js'; -import { - BackendIdSandboxResolver, - Sandbox, - SandboxDeleteOptions, - SandboxEvents, - SandboxOptions, -} from './sandbox.js'; -import parseGitIgnore from 'parse-gitignore'; -import path from 'path'; -import fs from 'fs'; -import _open from 'open'; -// EventEmitter is a class name and expected to have PascalCase -// eslint-disable-next-line @typescript-eslint/naming-convention -import EventEmitter from 'events'; -import { - GetParameterCommand, - ParameterNotFound, - SSMClient, - SSMServiceException, -} from '@aws-sdk/client-ssm'; -import { - AmplifyPrompter, - LogLevel, - Printer, - format, -} from '@aws-amplify/cli-core'; -import { - FilesChangesTracker, - createFilesChangesTracker, -} from './files_changes_tracker.js'; -import { - AmplifyError, - AmplifyUserError, - BackendIdentifierConversions, -} from '@aws-amplify/platform-core'; - -/** - * CDK stores bootstrap version in parameter store. Example parameter name looks like /cdk-bootstrap//version. - * The default value for qualifier is hnb659fds, i.e. default parameter path is /cdk-bootstrap/hnb659fds/version. - * The default qualifier is hardcoded value without any significance. - * Ability to provide custom qualifier is intended for name isolation between automated tests of the CDK itself. - * In order to use custom qualifier all stack synthesizers must be programmatically configured to use it. - * That makes bootstraps with custom qualifier incompatible with Amplify Backend and we treat that setup as - * not bootstrapped. - * See: https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html - */ -export const CDK_DEFAULT_BOOTSTRAP_VERSION_PARAMETER_NAME = - // suppress spell checker, it is triggered by qualifier value. - // eslint-disable-next-line spellcheck/spell-checker - '/cdk-bootstrap/hnb659fds/version'; -export const CDK_MIN_BOOTSTRAP_VERSION = 6; - -/** - * Constructs Amplify Console bootstrap URL for a given region - * @param region AWS region - * @returns Amplify Console bootstrap URL - */ -export const getBootstrapUrl = (region: string) => - `https://${region}.console.aws.amazon.com/amplify/create/bootstrap?region=${region}`; - -/** - * Runs a file watcher and deploys - */ -export class FileWatchingSandbox extends EventEmitter implements Sandbox { - private watcherSubscription: Awaited>; - private outputFilesExcludedFromWatch = ['.amplify']; - private filesChangesTracker: FilesChangesTracker; - - /** - * Creates a watcher process for this instance - */ - constructor( - private readonly backendIdSandboxResolver: BackendIdSandboxResolver, - private readonly executor: AmplifySandboxExecutor, - private readonly ssmClient: SSMClient, - private readonly printer: Printer, - private readonly open = _open - ) { - process.once('SIGINT', () => void this.stop()); - process.once('SIGTERM', () => void this.stop()); - super(); - } - - /** - * @inheritdoc - */ - override emit(eventName: SandboxEvents, ...args: unknown[]): boolean { - return super.emit(eventName, ...args); - } - - /** - * @inheritdoc - */ - override on( - eventName: SandboxEvents, - listener: (...args: unknown[]) => void - ): this { - return super.on(eventName, listener); - } - - /** - * @inheritdoc - */ - start = async (options: SandboxOptions) => { - const watchDir = options.dir ?? './amplify'; - const watchForChanges = options.watchForChanges ?? true; - - if (!fs.existsSync(watchDir)) { - throw new AmplifyUserError('PathNotFoundError', { - message: `${watchDir} does not exist.`, - resolution: - 'Make sure you are running this command from your project root directory.', - }); - } - - this.filesChangesTracker = await createFilesChangesTracker(watchDir); - const bootstrapped = await this.isBootstrapped(); - if (!bootstrapped) { - this.printer.log( - 'The given region has not been bootstrapped. Sign in to console as a Root user or Admin to complete the bootstrap process, then restart the sandbox.' - ); - // get region from an available sdk client; - const region = await this.ssmClient.config.region(); - await this.open(getBootstrapUrl(region)); - return; - } - - const ignoredPaths = this.getGitIgnoredPaths(); - this.outputFilesExcludedFromWatch = - this.outputFilesExcludedFromWatch.concat(...ignoredPaths); - - await this.printSandboxNameInfo(options.identifier); - - // Since 'cdk deploy' is a relatively slow operation for a 'watch' process, - // introduce a concurrency latch that tracks the state. - // This way, if file change events arrive when a 'cdk deploy' is still executing, - // we will batch them, and trigger another 'cdk deploy' after the current one finishes, - // making sure 'cdk deploy's always execute one at a time. - // Here's a diagram showing the state transitions: - - // -------- file changed -------------- file changed -------------- file changed - // | | ------------------> | | ------------------> | | --------------| - // | open | | deploying | | queued | | - // | | <------------------ | | <------------------ | | <-------------| - // -------- 'cdk deploy' done -------------- 'cdk deploy' done -------------- - - let latch: 'open' | 'deploying' | 'queued' = 'open'; - - const deployAndWatch = debounce(async () => { - latch = 'deploying'; - await this.deploy(options); - - // If latch is still 'deploying' after the 'await', that's fine, - // but if it's 'queued', that means we need to deploy again - while ((latch as 'deploying' | 'queued') === 'queued') { - // TypeScript doesn't realize latch can change between 'awaits' ¯\_(ツ)_/¯, - // and thinks the above 'while' condition is always 'false' without the cast - latch = 'deploying'; - this.printer.log( - "[Sandbox] Detected file changes while previous deployment was in progress. Invoking 'sandbox' again" - ); - await this.deploy(options); - } - latch = 'open'; - this.emitWatching(); - }); - - if (watchForChanges) { - this.watcherSubscription = await parcelWatcher.subscribe( - watchDir, - async (_, events) => { - // Log and track file changes. - await Promise.all( - events.map(({ type: eventName, path }) => { - this.filesChangesTracker.trackFileChange(path); - this.printer.log( - `[Sandbox] Triggered due to a file ${eventName} event: ${path}` - ); - }) - ); - if (latch === 'open') { - await deployAndWatch(); - } else { - // this means latch is either 'deploying' or 'queued' - latch = 'queued'; - this.printer.log( - '[Sandbox] Previous deployment is still in progress. ' + - 'Will queue for another deployment after this one finishes' - ); - } - }, - { - ignore: this.outputFilesExcludedFromWatch.concat( - ...(options.exclude ?? []) - ), - } - ); - // Start the first full deployment without waiting for a file change - await deployAndWatch(); - } else { - await this.deploy(options); - } - }; - - /** - * @inheritdoc - */ - stop = async () => { - this.printer.log(`[Sandbox] Shutting down`, LogLevel.DEBUG); - // can be undefined if command exits before subscription - await this.watcherSubscription?.unsubscribe(); - }; - - /** - * @inheritdoc - */ - delete = async (options: SandboxDeleteOptions) => { - this.printer.log( - '[Sandbox] Deleting all the resources in the sandbox environment...' - ); - await this.executor.destroy( - await this.backendIdSandboxResolver(options.identifier) - ); - this.emit('successfulDeletion'); - this.printer.log('[Sandbox] Finished deleting.'); - }; - - private shouldValidateAppSources = (): boolean => { - const snapshot = this.filesChangesTracker.getAndResetSnapshot(); - // if zero files changed this indicates initial deployment - const shouldValidateOnColdStart = - snapshot.hadTypeScriptFilesAtStart && - !snapshot.didAnyFileChangeSinceStart; - return ( - shouldValidateOnColdStart || - snapshot.didAnyTypeScriptFileChangeSinceLastSnapshot - ); - }; - - private deploy = async (options: SandboxOptions) => { - try { - const deployResult = await this.executor.deploy( - await this.backendIdSandboxResolver(options.identifier), - // It's important to pass this as callback so that debounce does - // not reset tracker prematurely - this.shouldValidateAppSources - ); - this.printer.log('[Sandbox] Deployment successful', LogLevel.DEBUG); - this.emit('successfulDeployment', deployResult); - } catch (error) { - // Print a meaningful message - this.printer.print(format.error(this.getErrorMessage(error))); - this.emit('failedDeployment', error); - - // If the error is because of a non-allowed destructive change such as - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html#cfn-cognito-userpool-aliasattributes - // offer to recreate the sandbox or revert the change - if ( - error instanceof AmplifyError && - error.name === 'CFNUpdateNotSupportedError' - ) { - await this.handleUnsupportedDestructiveChanges(options); - } - // else do not propagate and let the sandbox continue to run - } - }; - - private reset = async (options: SandboxOptions) => { - await this.delete({ identifier: options.identifier }); - await this.start(options); - }; - - /** - * Just a shorthand console log to indicate whenever watcher is going idle - */ - private emitWatching = () => { - this.printer.log(`[Sandbox] Watching for file changes...`); - }; - - /** - * Reads and parses .gitignore file and returns the list of paths - */ - private getGitIgnoredPaths = () => { - const gitIgnoreFilePath = path.join(process.cwd(), '.gitignore'); - if (fs.existsSync(gitIgnoreFilePath)) { - return parseGitIgnore - .parse(gitIgnoreFilePath) - .patterns.map((pattern: string) => - pattern.startsWith('/') ? pattern.substring(1) : pattern - ) - .filter((pattern: string) => { - if (pattern.startsWith('!')) { - this.printer.log( - `[Sandbox] Pattern ${pattern} found in .gitignore. "${pattern.substring( - 1 - )}" will not be watched if other patterns in .gitignore are excluding it.` - ); - return false; - } - return true; - }); - } - return []; - }; - - /** - * Checks if a given region has been bootstrapped with >= min version using CDK bootstrap version parameter - * stored in parameter store. - * @returns A Boolean that represents if region has been bootstrapped. - */ - private isBootstrapped = async () => { - try { - const { Parameter: parameter } = await this.ssmClient.send( - new GetParameterCommand({ - Name: CDK_DEFAULT_BOOTSTRAP_VERSION_PARAMETER_NAME, - }) - ); - - const bootstrapVersion = parameter?.Value; - if ( - !bootstrapVersion || - Number(bootstrapVersion) < CDK_MIN_BOOTSTRAP_VERSION - ) { - return false; - } - return true; - } catch (e) { - if (e instanceof ParameterNotFound) { - return false; - } - if ( - e instanceof SSMServiceException && - [ - 'UnrecognizedClientException', - 'AccessDeniedException', - 'NotAuthorized', - 'ExpiredTokenException', - ].includes(e.name) - ) { - throw new AmplifyUserError( - 'SSMCredentialsError', - { - message: `${e.name}: ${e.message}`, - resolution: - 'Make sure your AWS credentials are set up correctly and have permissions to call SSM:GetParameter', - }, - e - ); - } - - // If we are unable to retrieve bootstrap version parameter due to other reasons, we fail fast. - throw e; - } - }; - - /** - * Generates a printable error message from the thrown error - */ - private getErrorMessage = (error: unknown) => { - let message; - if (error instanceof Error) { - message = error.message; - - // Add the downstream exception - if (error.cause && error.cause instanceof Error && error.cause.message) { - message = `${message}\nCaused By: ${error.cause.message}\n`; - } - - if (error instanceof AmplifyError && error.resolution) { - message = `${message}\nResolution: ${error.resolution}\n`; - } - } else message = String(error); - return message; - }; - - private handleUnsupportedDestructiveChanges = async ( - options: SandboxOptions - ) => { - this.printer.print( - format.error( - '[Sandbox] We cannot deploy your new changes. You can either revert them or recreate your sandbox with the new changes (deleting all user data)' - ) - ); - // offer to recreate the sandbox with new properties - const answer = await AmplifyPrompter.yesOrNo({ - message: - 'Would you like to recreate your sandbox (deleting all user data)?', - defaultValue: false, - }); - if (answer) { - await this.stop(); - await this.reset(options); - } - // else let the sandbox continue so customers can revert their changes - }; - - private printSandboxNameInfo = async (sandboxIdentifier?: string) => { - const sandboxBackendId = await this.backendIdSandboxResolver( - sandboxIdentifier - ); - const stackName = - BackendIdentifierConversions.toStackName(sandboxBackendId); - this.printer.log( - format.indent(format.highlight(format.bold('\nAmplify Sandbox\n'))) - ); - this.printer.log( - format.indent(`${format.bold('Identifier:')} \t${sandboxBackendId.name}`) - ); - this.printer.log(format.indent(`${format.bold('Stack:')} \t${stackName}`)); - if (!sandboxIdentifier) { - this.printer.log( - `${format.indent( - format.dim('\nTo specify a different sandbox identifier, use ') - )}${format.bold('--identifier')}` - ); - } - }; -} From dae422abd733fbf3cd4cd446313c821452c2a081 Mon Sep 17 00:00:00 2001 From: fangyuwu7 Date: Sun, 14 Jul 2024 21:52:01 -0700 Subject: [PATCH 31/31] update unit test and bind function --- packages/auth-construct/src/construct.test.ts | 22 +++++++++++++++++-- packages/auth-construct/src/construct.ts | 6 +---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index c261a0d6d63..c82a1e7127b 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -613,7 +613,15 @@ void describe('Auth construct', () => { max: 66, min: 1, }, - 'custom:member_year': { + 'custom:register_date': { + dataType: 'DateTime', + mutable: true, + }, + 'custom:is_member': { + dataType: 'Boolean', + mutable: false, + }, + 'custom:year_as_member': { dataType: 'Number', max: 90, min: 0, @@ -660,9 +668,19 @@ void describe('Auth construct', () => { MinValue: '1', }, }, + { + AttributeDataType: 'DateTime', + Name: 'register_date', + Mutable: true, + }, + { + AttributeDataType: 'Boolean', + Name: 'is_member', + Mutable: false, + }, { AttributeDataType: 'Number', - Name: 'member_year', + Name: 'year_as_member', Mutable: true, NumberAttributeConstraints: { MaxValue: '90', diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 776ef0ae126..9a0e85206cb 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -368,8 +368,6 @@ export class AmplifyAuth // Conditionally add constraint properties based on dataType. if (attribute.dataType === 'String') { constraints = { - ...constraints, - stringConstraints: { minLen: attribute.minLen, @@ -378,8 +376,6 @@ export class AmplifyAuth }; } else if (attribute.dataType === 'Number') { constraints = { - ...constraints, - numberConstraints: { min: attribute.min, @@ -470,7 +466,7 @@ export class AmplifyAuth [key, value] ) => { if (key.startsWith('custom:')) { - const attributeKey = key.replace(/^(custom:|User\.?)/i, ''); + const attributeKey = key.replace(/^custom:/i, ''); acc.customAttributes[attributeKey] = this.bindCustomAttribute( attributeKey, value