diff --git a/infra-gen2/backends/analytics/main/.gitignore b/infra-gen2/backends/analytics/main/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/analytics/main/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/analytics/main/amplify/auth/pre-sign-up-handler.ts b/infra-gen2/backends/analytics/main/amplify/auth/pre-sign-up-handler.ts new file mode 100644 index 0000000000..ddf9e2d891 --- /dev/null +++ b/infra-gen2/backends/analytics/main/amplify/auth/pre-sign-up-handler.ts @@ -0,0 +1,4 @@ +import { PreSignUpTriggerHandler } from "aws-lambda"; +import { preSignUpTriggerHandler } from "infra-common"; + +export const handler: PreSignUpTriggerHandler = preSignUpTriggerHandler; diff --git a/infra-gen2/backends/analytics/main/amplify/auth/resource.ts b/infra-gen2/backends/analytics/main/amplify/auth/resource.ts new file mode 100644 index 0000000000..d9ccac9178 --- /dev/null +++ b/infra-gen2/backends/analytics/main/amplify/auth/resource.ts @@ -0,0 +1,19 @@ +import { defineAuth, defineFunction } from "@aws-amplify/backend"; + +export const preSignUp = defineFunction({ + name: "pre-sign-up", + entry: "./pre-sign-up-handler.ts", +}); + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true, + }, + triggers: { + preSignUp, + }, +}); diff --git a/infra-gen2/backends/analytics/main/amplify/backend.ts b/infra-gen2/backends/analytics/main/amplify/backend.ts new file mode 100644 index 0000000000..f6f234db2d --- /dev/null +++ b/infra-gen2/backends/analytics/main/amplify/backend.ts @@ -0,0 +1,28 @@ +import { defineBackend } from "@aws-amplify/backend"; +import { addAnalyticsExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +/** + * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more + */ + +const backend = defineBackend({ + auth, +}); + +const pinpointRole = backend.auth.resources.authenticatedUserIamRole; +const unauthPinpointRole = backend.auth.resources.unauthenticatedUserIamRole; + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; + +const customOutputs = addAnalyticsExtensions({ + name: "analytics-main", + stack: stack, + authenticatedRole: pinpointRole, + unauthenticatedRole: unauthPinpointRole, +}); + +// patch the custom Pinpoint resource to the expected output configuration +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/analytics/main/amplify/package.json b/infra-gen2/backends/analytics/main/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/analytics/main/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/analytics/main/amplify/tsconfig.json b/infra-gen2/backends/analytics/main/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/analytics/main/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/analytics/main/package.json b/infra-gen2/backends/analytics/main/package.json new file mode 100644 index 0000000000..3421bd75c5 --- /dev/null +++ b/infra-gen2/backends/analytics/main/package.json @@ -0,0 +1,5 @@ +{ + "name": "analytics-main", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/analytics/main/schema.graphql b/infra-gen2/backends/analytics/main/schema.graphql new file mode 100644 index 0000000000..e9c57e41f7 --- /dev/null +++ b/infra-gen2/backends/analytics/main/schema.graphql @@ -0,0 +1,22 @@ +type Query { + getRecord(id: ID!): Record @aws_api_key + listRecords: [Record] @aws_api_key +} + +type Mutation { + createRecord(input: RecordInput!): Record @aws_api_key +} + +type Subscription { + onCreateRecord: Record @aws_subscribe(mutations: ["createRecord"]) +} + +type Record { + id: ID! + payload: String! +} + +input RecordInput { + id: ID! + payload: String! +} diff --git a/infra-gen2/backends/analytics/no-unauth-access/.gitignore b/infra-gen2/backends/analytics/no-unauth-access/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-access/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/analytics/no-unauth-access/amplify/auth/pre-sign-up-handler.ts b/infra-gen2/backends/analytics/no-unauth-access/amplify/auth/pre-sign-up-handler.ts new file mode 100644 index 0000000000..ddf9e2d891 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-access/amplify/auth/pre-sign-up-handler.ts @@ -0,0 +1,4 @@ +import { PreSignUpTriggerHandler } from "aws-lambda"; +import { preSignUpTriggerHandler } from "infra-common"; + +export const handler: PreSignUpTriggerHandler = preSignUpTriggerHandler; diff --git a/infra-gen2/backends/analytics/no-unauth-access/amplify/auth/resource.ts b/infra-gen2/backends/analytics/no-unauth-access/amplify/auth/resource.ts new file mode 100644 index 0000000000..d9ccac9178 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-access/amplify/auth/resource.ts @@ -0,0 +1,19 @@ +import { defineAuth, defineFunction } from "@aws-amplify/backend"; + +export const preSignUp = defineFunction({ + name: "pre-sign-up", + entry: "./pre-sign-up-handler.ts", +}); + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true, + }, + triggers: { + preSignUp, + }, +}); diff --git a/infra-gen2/backends/analytics/no-unauth-access/amplify/backend.ts b/infra-gen2/backends/analytics/no-unauth-access/amplify/backend.ts new file mode 100644 index 0000000000..9603a0e48d --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-access/amplify/backend.ts @@ -0,0 +1,26 @@ +import { defineBackend } from "@aws-amplify/backend"; +import { addAnalyticsExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +/** + * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more + */ + +const backend = defineBackend({ + auth, +}); + +const pinpointRole = backend.auth.resources.authenticatedUserIamRole; + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; + +const customOutputs = addAnalyticsExtensions({ + name: "analytics-main", + stack: stack, + authenticatedRole: pinpointRole, +}); + +// patch the custom Pinpoint resource to the expected output configuration +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/analytics/no-unauth-access/amplify/package.json b/infra-gen2/backends/analytics/no-unauth-access/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-access/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/analytics/no-unauth-access/amplify/tsconfig.json b/infra-gen2/backends/analytics/no-unauth-access/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-access/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/analytics/no-unauth-access/package.json b/infra-gen2/backends/analytics/no-unauth-access/package.json new file mode 100644 index 0000000000..e460e65db5 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-access/package.json @@ -0,0 +1,5 @@ +{ + "name": "no-unauth-access", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/analytics/no-unauth-identities/.gitignore b/infra-gen2/backends/analytics/no-unauth-identities/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-identities/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/analytics/no-unauth-identities/amplify/auth/pre-sign-up-handler.ts b/infra-gen2/backends/analytics/no-unauth-identities/amplify/auth/pre-sign-up-handler.ts new file mode 100644 index 0000000000..ddf9e2d891 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-identities/amplify/auth/pre-sign-up-handler.ts @@ -0,0 +1,4 @@ +import { PreSignUpTriggerHandler } from "aws-lambda"; +import { preSignUpTriggerHandler } from "infra-common"; + +export const handler: PreSignUpTriggerHandler = preSignUpTriggerHandler; diff --git a/infra-gen2/backends/analytics/no-unauth-identities/amplify/auth/resource.ts b/infra-gen2/backends/analytics/no-unauth-identities/amplify/auth/resource.ts new file mode 100644 index 0000000000..d9ccac9178 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-identities/amplify/auth/resource.ts @@ -0,0 +1,19 @@ +import { defineAuth, defineFunction } from "@aws-amplify/backend"; + +export const preSignUp = defineFunction({ + name: "pre-sign-up", + entry: "./pre-sign-up-handler.ts", +}); + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true, + }, + triggers: { + preSignUp, + }, +}); diff --git a/infra-gen2/backends/analytics/no-unauth-identities/amplify/backend.ts b/infra-gen2/backends/analytics/no-unauth-identities/amplify/backend.ts new file mode 100644 index 0000000000..7405940e3c --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-identities/amplify/backend.ts @@ -0,0 +1,31 @@ +import { defineBackend } from "@aws-amplify/backend"; +import { addAnalyticsExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +/** + * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more + */ + +const backend = defineBackend({ + auth, +}); + +const { cfnIdentityPool } = backend.auth.resources.cfnResources; +cfnIdentityPool.allowUnauthenticatedIdentities = false; + +const pinpointRole = backend.auth.resources.authenticatedUserIamRole; +const unauthPinpointRole = backend.auth.resources.unauthenticatedUserIamRole; + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; + +const customOutputs = addAnalyticsExtensions({ + name: "analytics-main", + stack: stack, + authenticatedRole: pinpointRole, + unauthenticatedRole: unauthPinpointRole, +}); + +// patch the custom Pinpoint resource to the expected output configuration +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/analytics/no-unauth-identities/amplify/package.json b/infra-gen2/backends/analytics/no-unauth-identities/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-identities/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/analytics/no-unauth-identities/amplify/tsconfig.json b/infra-gen2/backends/analytics/no-unauth-identities/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-identities/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/analytics/no-unauth-identities/package.json b/infra-gen2/backends/analytics/no-unauth-identities/package.json new file mode 100644 index 0000000000..63d942b8af --- /dev/null +++ b/infra-gen2/backends/analytics/no-unauth-identities/package.json @@ -0,0 +1,5 @@ +{ + "name": "no-unauth-identities", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/infra-common/src/analytics-extensions/README.md b/infra-gen2/infra-common/src/analytics-extensions/README.md new file mode 100644 index 0000000000..f2e4278758 --- /dev/null +++ b/infra-gen2/infra-common/src/analytics-extensions/README.md @@ -0,0 +1,9 @@ +# analytics-extensions + +This directory contains extensions for the analytics backend, including: + +- Setting up Pinpoint +- Setting up a Kinesis Data stream and configure Pinpoint to stream events to it +- Create a Records table to store events from the Kinesis stream +- Create the GraphQL API to which we will publish Kinesis records +- Create the Kinesis consumer Lambda which will capture events from the Kinesis Data Stream and forward them to AppSync. diff --git a/infra-gen2/infra-common/src/analytics-extensions/analytics-extensions.ts b/infra-gen2/infra-common/src/analytics-extensions/analytics-extensions.ts new file mode 100644 index 0000000000..f0691703d8 --- /dev/null +++ b/infra-gen2/infra-common/src/analytics-extensions/analytics-extensions.ts @@ -0,0 +1,47 @@ +import { BackendBase } from "@aws-amplify/backend"; +import { Stack } from "aws-cdk-lib"; +import { IRole } from "aws-cdk-lib/aws-iam"; +import { createAppSyncAPI } from "./data"; +import { createKinesisStream } from "./kinesis"; +import { createPinpointApp } from "./pinpoint"; + +type AmplifyOutputs = Parameters[0]; + +export const addAnalyticsExtensions = ({ + name, + stack, + authenticatedRole, + unauthenticatedRole, +}: { + name: string; + stack: Stack; + authenticatedRole: IRole; + unauthenticatedRole?: IRole; +}): AmplifyOutputs => { + const pinpointApp = createPinpointApp( + name, + stack, + authenticatedRole, + unauthenticatedRole + ); + + const kinesisStream = createKinesisStream(name, stack, pinpointApp); + + const graphQLApi = createAppSyncAPI(name, stack, kinesisStream); + + return { + analytics: { + amazon_pinpoint: { + app_id: pinpointApp.ref, + aws_region: Stack.of(pinpointApp).region, + }, + }, + data: { + aws_region: stack.region, + url: graphQLApi.graphqlUrl, + api_key: graphQLApi.apiKey, + default_authorization_type: "API_KEY", + authorization_types: [], + }, + }; +}; diff --git a/infra-gen2/infra-common/src/analytics-extensions/data.ts b/infra-gen2/infra-common/src/analytics-extensions/data.ts new file mode 100644 index 0000000000..279d61bfce --- /dev/null +++ b/infra-gen2/infra-common/src/analytics-extensions/data.ts @@ -0,0 +1,110 @@ +import * as appsync from "aws-cdk-lib/aws-appsync"; +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; +import { Stream } from "aws-cdk-lib/aws-kinesis"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as lambda_events from "aws-cdk-lib/aws-lambda-event-sources"; +import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; +import { RemovalPolicy, Stack } from "aws-cdk-lib/core"; +import path from "path"; +import { inOneYear } from "../expiration"; + +export function createAppSyncAPI( + name: string, + stack: Stack, + kinesisStream: Stream +) { + // Create the Records table to store events received from the + // Kinesis stream. + + const recordsTable = new dynamodb.Table(stack, "RecordsTable" + name, { + removalPolicy: RemovalPolicy.DESTROY, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + partitionKey: { + type: dynamodb.AttributeType.STRING, + name: "id", + }, + }); + + // Create the GraphQL API to which we will publish Kinesis records + // Adapted from: https://github.com/Focus-Otter/appsync-simple-auth-backend + + const authorizationType = appsync.AuthorizationType.API_KEY; + const graphQLApi = new appsync.GraphqlApi(stack, "GraphQLApi" + name, { + name: stack.stackName, + definition: { + schema: appsync.SchemaFile.fromAsset( + path.resolve(__dirname, "..", "schemas", "analytics-schema.graphql") + ), + }, + authorizationConfig: { + defaultAuthorization: { + authorizationType, + apiKeyConfig: { + expires: inOneYear(), + }, + }, + }, + logConfig: { + fieldLogLevel: appsync.FieldLogLevel.ALL, + excludeVerboseContent: false, + }, + }); + + graphQLApi + .addDynamoDbDataSource("GraphQLApiGetRecord", recordsTable) + .createResolver("QueryGetRecordResolver", { + typeName: "Query", + fieldName: "getRecord", + requestMappingTemplate: appsync.MappingTemplate.dynamoDbGetItem( + "id", + "id" + ), + responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(), + }); + + graphQLApi + .addDynamoDbDataSource("GraphQLApiListRecords", recordsTable) + .createResolver("QueryListRecordsResolver", { + typeName: "Query", + fieldName: "listRecords", + requestMappingTemplate: appsync.MappingTemplate.dynamoDbScanTable(), + responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(), + }); + + graphQLApi + .addDynamoDbDataSource("GraphQLApiCreateRecord", recordsTable) + .createResolver("MutationCreateRecordResolver", { + typeName: "Mutation", + fieldName: "createRecord", + requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem( + appsync.PrimaryKey.partition("id").is("input.id"), + appsync.Values.projecting("input") + ), + responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(), + }); + + // Create the Kinesis consumer Lambda which will capture events from the + // Kinesis Data Stream and forward them to AppSync. + + const kinesisConsumer = new NodejsFunction(stack, "kinesis-consumer", { + entry: path.resolve( + __dirname, + "..", + "lambda-triggers", + "kinesis-consumer.js" + ), + runtime: lambda.Runtime.NODEJS_20_X, + environment: { + GRAPHQL_API_ENDPOINT: graphQLApi.graphqlUrl, + GRAPHQL_API_KEY: graphQLApi.apiKey!, + }, + }); + + const eventSource = new lambda_events.KinesisEventSource(kinesisStream, { + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + }); + + kinesisConsumer.addEventSource(eventSource); + + return graphQLApi; +} diff --git a/infra-gen2/infra-common/src/analytics-extensions/kinesis.ts b/infra-gen2/infra-common/src/analytics-extensions/kinesis.ts new file mode 100644 index 0000000000..5a0fbe97e5 --- /dev/null +++ b/infra-gen2/infra-common/src/analytics-extensions/kinesis.ts @@ -0,0 +1,48 @@ +import * as iam from "aws-cdk-lib/aws-iam"; +import * as kinesis from "aws-cdk-lib/aws-kinesis"; +import { CfnApp, CfnEventStream } from "aws-cdk-lib/aws-pinpoint"; +import { Stack } from "aws-cdk-lib/core"; + +export function createKinesisStream( + name: string, + stack: Stack, + pinpointApp: CfnApp +) { + const kinesisStream = new kinesis.Stream(stack, "KinesisStream" + name, { + shardCount: 1, + }); + + // Necessary IAM permissions are described here: + // https://docs.aws.amazon.com/pinpoint/latest/developerguide/permissions-streams.html + const eventStreamRole = new iam.Role(stack, "EventStreamRole", { + assumedBy: new iam.ServicePrincipal("pinpoint.amazonaws.com", { + conditions: { + StringEquals: { + "aws:SourceAccount": Stack.of(stack).account, + }, + ArnLike: { + "aws:SourceArn": pinpointApp.attrArn, + }, + }, + }), + inlinePolicies: { + "pinpoint-export-to-kinesis": new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["kinesis:PutRecords", "kinesis:DescribeStream"], + resources: [kinesisStream.streamArn], + }), + ], + }), + }, + }); + + new CfnEventStream(stack, "PinPointEventStream", { + applicationId: pinpointApp.ref, + destinationStreamArn: kinesisStream.streamArn, + roleArn: eventStreamRole.roleArn, + }); + + return kinesisStream; +} diff --git a/infra-gen2/infra-common/src/analytics-extensions/pinpoint.ts b/infra-gen2/infra-common/src/analytics-extensions/pinpoint.ts new file mode 100644 index 0000000000..fc0792886c --- /dev/null +++ b/infra-gen2/infra-common/src/analytics-extensions/pinpoint.ts @@ -0,0 +1,35 @@ +import { IRole, Policy, PolicyStatement } from "aws-cdk-lib/aws-iam"; +import { CfnApp } from "aws-cdk-lib/aws-pinpoint"; +import { Stack } from "aws-cdk-lib/core"; + +export function createPinpointApp( + name: string, + stack: Stack, + pinpointRole: IRole, + unauthPinpointRole?: IRole +) { + const pinpointApp = new CfnApp(stack, "PinpointApp", { + name: name, + }); + + const pinpointPolicy = new Policy(stack, "PinpointPolicy", { + policyName: "PinpointPolicy", + statements: [ + new PolicyStatement({ + actions: [ + "mobiletargeting:UpdateEndpoint", + "mobiletargeting:PutEvents", + ], + resources: [pinpointApp.attrArn + "/*"], + }), + ], + }); + + pinpointRole.attachInlinePolicy(pinpointPolicy); + + if (unauthPinpointRole) { + unauthPinpointRole.attachInlinePolicy(pinpointPolicy); + } + + return pinpointApp; +} diff --git a/infra-gen2/infra-common/src/index.ts b/infra-gen2/infra-common/src/index.ts index 6b5ccb2cfc..9843543a06 100644 --- a/infra-gen2/infra-common/src/index.ts +++ b/infra-gen2/infra-common/src/index.ts @@ -1,4 +1,9 @@ +import { addAnalyticsExtensions } from "./analytics-extensions/analytics-extensions"; import { addAuthUserExtensions } from "./auth-user-extensions/auth-user-extensions"; import { preSignUpTriggerHandler } from "./lambda-triggers/pre-sign-up"; -export { addAuthUserExtensions, preSignUpTriggerHandler }; +export { + addAnalyticsExtensions, + addAuthUserExtensions, + preSignUpTriggerHandler, +}; diff --git a/infra-gen2/infra-common/src/lambda-triggers/kinesis-consumer.ts b/infra-gen2/infra-common/src/lambda-triggers/kinesis-consumer.ts new file mode 100644 index 0000000000..6e4d893d24 --- /dev/null +++ b/infra-gen2/infra-common/src/lambda-triggers/kinesis-consumer.ts @@ -0,0 +1,60 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { KinesisStreamEvent, KinesisStreamHandler } from "aws-lambda"; +import fetch from "node-fetch"; + +const GRAPHQL_API_ENDPOINT = process.env.GRAPHQL_API_ENDPOINT!; +const GRAPHQL_API_KEY = process.env.GRAPHQL_API_KEY!; + +export const handler: KinesisStreamHandler = async ( + event: KinesisStreamEvent +) => { + console.log(`Got event: ${JSON.stringify(event, null, 2)}`); + + await Promise.all( + event.Records.map(async (record) => { + const payload = Buffer.from(record.kinesis.data, "base64").toString( + "ascii" + ); + console.log(`Got record: ${JSON.stringify(record, null, 2)}`); + console.log(`Decoded payload: ${payload}`); + + try { + const resp = await fetch(GRAPHQL_API_ENDPOINT, { + method: "POST", + headers: { + "x-api-key": GRAPHQL_API_KEY, + }, + body: JSON.stringify({ + query: ` + mutation CreateRecord($id: ID!, $payload: String!) { + createRecord(input: { + id: $id + payload: $payload + }) { + id + payload + } + } + `, + variables: { + id: record.eventID, + payload: payload, + }, + }), + }); + const json = await resp.json(); + console.log( + `Successfully forwarded event ${record.eventID}: ${JSON.stringify( + json, + null, + 2 + )}` + ); + } catch (error) { + console.error("Could not POST to GraphQL endpoint: ", error); + } + }) + ); +}; diff --git a/infra-gen2/infra-common/src/schemas/analytics-schema.graphql b/infra-gen2/infra-common/src/schemas/analytics-schema.graphql new file mode 100644 index 0000000000..e9c57e41f7 --- /dev/null +++ b/infra-gen2/infra-common/src/schemas/analytics-schema.graphql @@ -0,0 +1,22 @@ +type Query { + getRecord(id: ID!): Record @aws_api_key + listRecords: [Record] @aws_api_key +} + +type Mutation { + createRecord(input: RecordInput!): Record @aws_api_key +} + +type Subscription { + onCreateRecord: Record @aws_subscribe(mutations: ["createRecord"]) +} + +type Record { + id: ID! + payload: String! +} + +input RecordInput { + id: ID! + payload: String! +} diff --git a/infra-gen2/package-lock.json b/infra-gen2/package-lock.json index e49338ae82..99c9936b0d 100644 --- a/infra-gen2/package-lock.json +++ b/infra-gen2/package-lock.json @@ -29,7 +29,24 @@ "typescript": "^5.5.4" } }, - "backends/api/api-multi-auth": { + "backends/analytics/main": { + "name": "analytics-main", + "version": "1.0.0" + }, + "backends/analytics/no-unauth-access": { + "version": "1.0.0" + }, + "backends/analytics/no-unauth-identities": { + "version": "1.0.0" + }, + "backends/api/apiMultiAuth": { + "name": "api-multi-auth", + "version": "1.0.0" + }, + "backends/storage/dots-in-name": { + "version": "1.0.0" + }, + "backends/storage/main": { "version": "1.0.0" }, "backends/auth/email-sign-in": { @@ -16376,6 +16393,10 @@ "node": ">=18.0.0" } }, + "node_modules/analytics-main": { + "resolved": "backends/analytics/main", + "link": true + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -19974,6 +19995,14 @@ "tslib": "^2.0.3" } }, + "node_modules/no-unauth-access": { + "resolved": "backends/analytics/no-unauth-access", + "link": true + }, + "node_modules/no-unauth-identities": { + "resolved": "backends/analytics/no-unauth-identities", + "link": true + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", diff --git a/infra-gen2/tool/deploy_gen2.dart b/infra-gen2/tool/deploy_gen2.dart index da44078596..d93f94eb0d 100644 --- a/infra-gen2/tool/deploy_gen2.dart +++ b/infra-gen2/tool/deploy_gen2.dart @@ -71,6 +71,27 @@ const List infraConfig = [ ), ], ), + AmplifyBackendGroup( + category: Category.analytics, + defaultOutput: 'packages/analytics/amplify_analytics_pinpoint/example/lib', + backends: [ + AmplifyBackend( + name: 'main', + identifier: 'main', + pathToSource: 'infra-gen2/backends/analytics/main', + ), + AmplifyBackend( + name: 'no-unauth-access', + identifier: 'no-unauth-acc', + pathToSource: 'infra-gen2/backends/analytics/no-unauth-access', + ), + AmplifyBackend( + name: 'no-unauth-identities', + identifier: 'no-unauth-id', + pathToSource: 'infra-gen2/backends/analytics/no-unauth-identities', + ), + ], + ), ]; const pathToBackends = 'infra-gen2/backends'; diff --git a/packages/analytics/amplify_analytics_pinpoint/example/integration_test/no_unauth_test.dart b/packages/analytics/amplify_analytics_pinpoint/example/integration_test/no_unauth_test.dart index e9c32096ab..8c45d13f42 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/integration_test/no_unauth_test.dart +++ b/packages/analytics/amplify_analytics_pinpoint/example/integration_test/no_unauth_test.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'package:amplify_analytics_pinpoint/amplify_analytics_pinpoint.dart'; -import 'package:amplify_analytics_pinpoint_example/amplifyconfiguration.dart'; +import 'package:amplify_analytics_pinpoint_example/amplify_outputs.dart'; import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; diff --git a/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/mock_secure_storage.dart b/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/mock_secure_storage.dart index a0517843b5..04485de72c 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/mock_secure_storage.dart +++ b/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/mock_secure_storage.dart @@ -1,12 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// ignore_for_file: avoid_dynamic_calls + import 'dart:async'; import 'dart:convert'; import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/endpoint_client/endpoint_info_store_manager.dart'; import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/endpoint_client/endpoint_store_keys.dart'; -import 'package:amplify_analytics_pinpoint_example/amplifyconfiguration.dart'; +import 'package:amplify_analytics_pinpoint_example/amplify_outputs.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; /// Static key/value storage for use in integration tests. @@ -16,10 +18,10 @@ import 'package:amplify_flutter/amplify_flutter.dart'; SecureStorageInterface setupAndCreateMockPersistedSecuredStorage({ String? endpointId, }) { - final json = jsonDecode(amplifyconfig) as Map; - final amplifyConfig = AmplifyConfig.fromJson(json.cast()); + final main = amplifyEnvironments['main']!; + final environment = jsonDecode(main) as Map; final pinpointAppId = - amplifyConfig.analytics!.awsPlugin!.pinpointAnalytics.appId; + environment['analytics']?['amazon_pinpoint']?['app_id'] as String; final storage = _MockSecureStorage(); diff --git a/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/setup_utils.dart b/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/setup_utils.dart index 9c8553630c..330c41e3ac 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/setup_utils.dart +++ b/packages/analytics/amplify_analytics_pinpoint/example/integration_test/utils/setup_utils.dart @@ -18,7 +18,7 @@ import 'dart:convert'; import 'package:amplify_analytics_pinpoint/amplify_analytics_pinpoint.dart'; import 'package:amplify_analytics_pinpoint_dart/src/impl/flutter_provider_interfaces/app_lifecycle_provider.dart'; -import 'package:amplify_analytics_pinpoint_example/amplifyconfiguration.dart'; +import 'package:amplify_analytics_pinpoint_example/amplify_outputs.dart'; import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; diff --git a/packages/analytics/amplify_analytics_pinpoint/example/ios/Flutter/AppFrameworkInfo.plist b/packages/analytics/amplify_analytics_pinpoint/example/ios/Flutter/AppFrameworkInfo.plist index 9625e105df..7c56964006 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/analytics/amplify_analytics_pinpoint/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/project.pbxproj b/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/project.pbxproj index cd8506020d..e069e196b9 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a335..5e31d3d342 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/analytics/amplify_analytics_pinpoint/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { ]); try { - await Amplify.configure(amplifyconfig); + await Amplify.configure(amplifyConfig); setState(() { _amplifyConfigured = true; }); @@ -98,6 +98,7 @@ class _MyAppState extends State { await Amplify.Analytics.registerGlobalProperties( globalProperties: properties, ); + safePrint('registered global properties: $_globalProp'); } void _unregisterGlobalProperties() async { diff --git a/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/project.pbxproj b/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/project.pbxproj index 997c168f6d..8ed8c9ed07 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/project.pbxproj @@ -202,7 +202,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6ce263448f..d068f85ce9 100644 --- a/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/analytics/amplify_analytics_pinpoint/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@