Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 85 additions & 6 deletions cdk/cdk-workshop-stack.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { LambdaIntegration, RestApi } from '@aws-cdk/aws-apigateway';
import { LambdaIntegration, RestApi, CfnIntegrationV2, CfnRouteV2, CfnApiV2 } from '@aws-cdk/aws-apigateway';
// import * as gw from '@aws-cdk/aws-apigatewayv2';
import { CloudFrontWebDistribution, CloudFrontWebDistributionProps, PriceClass } from '@aws-cdk/aws-cloudfront';
import { CfnOutput, Construct, Duration, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core';
import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb';
import { Code, Function, LayerVersion, Runtime } from '@aws-cdk/aws-lambda';
import { AttributeType, BillingMode, Table, StreamViewType } from '@aws-cdk/aws-dynamodb';
import { Code, Function, LayerVersion, Runtime, StartingPosition } from '@aws-cdk/aws-lambda';
import { Bucket, EventType } from '@aws-cdk/aws-s3';
import { BucketDeployment, Source } from '@aws-cdk/aws-s3-deployment';
import { LambdaDestination } from '@aws-cdk/aws-s3-notifications';
import { path as rootPath } from 'app-root-path';
import { resolve } from 'path';
import { Topic } from '@aws-cdk/aws-sns';
// import * as subs from '@aws-cdk/aws-sns-subscriptions'
// import { DynamoEventSource } from '@aws-cdk/aws-lambda-event-sources'

import { PolicyStatement, Effect, Role, ServicePrincipal } from '@aws-cdk/aws-iam';

import { addCorsOptions } from './cors.utils';
import { WebIndex } from './web-index';
Expand All @@ -17,14 +23,21 @@ export interface CdkWorkshopStackProps extends StackProps {
}

export class CdkWorkshopStack extends Stack {
constructor(scope: Construct, id: string, props: CdkWorkshopStackProps) {
constructor (scope: Construct, id: string, props: CdkWorkshopStackProps) {
super(scope, id, props);

// API

const topic = new Topic(this, 'Topic', {
displayName: 'Pin topic',
topicName: 'PinTopic'
});

const imageBucket = new Bucket(this, 'ImageBucket');

const pinTable = new Table(this, 'PinTable', {
replicationRegions: [process.env.AWS_REGION as string],
stream: StreamViewType.NEW_IMAGE,
partitionKey: {
name: 'pointUrl',
type: AttributeType.STRING
Expand All @@ -47,9 +60,40 @@ export class CdkWorkshopStack extends Stack {
handler: 'pin-lambda.handler',
environment: {
IMAGE_BUCKET: imageBucket.bucketName,
PIN_TABLE: pinTable.tableName
PIN_TABLE: pinTable.tableName,
PIN_TOPIC: topic.topicArn ?? 'notopic'
}
});

const pinSocketFunction = new Function(this, 'PinSocket', {
code: apiCode,
runtime: Runtime.NODEJS_12_X,
handler: 'pin-socket-lambda.handler',
environment: {
PIN_STREAM: pinTable.tableName,
PIN_TOPIC: topic.topicArn
}
});

pinTable.grantStream(pinSocketFunction);
// pinSocketFunction.addEventSource(
// new DynamoEventSource(pinTableStream, {
// startingPosition: StartingPosition.LATEST,
// batchSize: 10
// })
// )

pinSocketFunction.addEventSourceMapping('PinStream', {
eventSourceArn: pinTable.tableStreamArn as string,
startingPosition: StartingPosition.LATEST
});

// new EventSourceMapping(this, 'PinStream', {
// eventSourceArn: pinTableStream.tableStreamArn,
// target: pinSocketFunction,
// startingPosition: StartingPosition.LATEST
// })

imageBucket.grantReadWrite(pinHandler);
pinTable.grantReadWriteData(pinHandler);

Expand Down Expand Up @@ -89,6 +133,41 @@ export class CdkWorkshopStack extends Stack {

const pinApi = api.root.addResource('pin');

const wsApiGw = new CfnApiV2(this, 'pinSocketGw', {
name: 'pinSocketGw',
protocolType: 'WEBSOCKET',
routeSelectionExpression: '$request.body.message'
});
const apiId = wsApiGw.ref

const policy = new PolicyStatement({
effect: Effect.ALLOW,
resources: [pinHandler.functionArn],
actions: ['lambda:InvokeFunction']
});

const wsRole = new Role(this, 'socketGwRole', {
assumedBy: new ServicePrincipal('apigateway.amazonaws.com')
});

wsRole.addToPolicy(policy);

const wsConnectIntegreation = new CfnIntegrationV2(
this,
'wsConnectIntegreation',
{
apiId,
integrationType: 'AWS_PROXY',
integrationUri: `arn:aws:apigateway:${process.env.AWS_REGION}:lambda:path/2015-03-31/functions/${pinHandler.functionArn}/invocations`,
credentialsArn: wsRole.roleArn
}
);
const wsConnect = new CfnRouteV2(this, 'pinSocketConnect', {
apiId,
routeKey: '$connect',
target: `integrations${wsConnectIntegreation.ref}`
});

// OPTIONS /pin
addCorsOptions(pinApi);
// ANY /pin
Expand Down Expand Up @@ -120,7 +199,7 @@ export class CdkWorkshopStack extends Stack {
apiBaseUrl: api.url,
source: webSource,
bucket: webBucket
});
})

webIndex.node.addDependency(webDeployment);

Expand Down
43 changes: 36 additions & 7 deletions lib/api/pin-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
import { DynamoDB, SNS } from 'aws-sdk';
import { parse } from 'path';
import { v4 } from 'uuid';

Expand All @@ -13,8 +13,10 @@ const dynamo = new DynamoDB.DocumentClient({
endpoint: process.env.DYNAMODB_ENDPOINT
});

const sns = new SNS({ apiVersion: '2010-03-31' });

export async function handler(event: APIGatewayProxyEvent, context: Context) {
context.callbackWaitsForEmptyEventLoop = false;
context.callbackWaitsForEmptyEventLoop = false

const httpMethod = event.httpMethod.toUpperCase();
const pointUrl = event.pathParameters && event.pathParameters.pointUrl;
Expand All @@ -24,16 +26,14 @@ export async function handler(event: APIGatewayProxyEvent, context: Context) {

if (httpMethod === 'POST' && !pointUrl) {
return await handleSave(event, sourceIp);

} else if (httpMethod === 'PATCH' && pointUrl) {
return await handleRename(event, pointUrl);
} else if (httpMethod === 'GET' && !pointUrl) {
return await handleList();

} else if (httpMethod === 'GET' && pointUrl) {
return await handleGet(pointUrl);

} else if (httpMethod === 'DELETE' && pointUrl) {
return await handleDelete(pointUrl);

} else {
return transformResult({ statusCode: 404, body: { error: 'method/path not supported'} });
}
Expand Down Expand Up @@ -64,9 +64,38 @@ async function handleSave(event: APIGatewayProxyEvent, sourceIp: string) {
await dynamo.put({ TableName: pinTable, Item: savedPin }).promise();

const savedPinWithUrl = resolveSignedUrl(savedPin);
console.log('topiccArn: ', process.env.PIN_TOPIC);
sns.publish(
{
TopicArn: process.env.PIN_TOPIC,
Message: JSON.stringify(savedPinWithUrl)
},
console.log
)
return transformResult({ body: savedPinWithUrl });
}

async function handleRename(event: APIGatewayProxyEvent, pointUrl: string) {
const { customName } = JSON.parse(event.body as string) as Partial<Pin>

const params: DynamoDB.DocumentClient.UpdateItemInput = {
TableName: pinTable,
Key: { pointUrl },
UpdateExpression: 'set #customName = :customName',
ExpressionAttributeValues: {
':customName': customName
},
ExpressionAttributeNames: {
'#customName': 'customName'
},
ReturnValues: 'ALL_NEW'
}
const resp = await dynamo.update(params).promise();

const body = resolveSignedUrl(resp.Attributes as SavedPin);
return transformResult({ body });
}

// GET:/pin
async function handleList() {
const result = await dynamo.scan({ TableName: pinTable }).promise();
Expand All @@ -91,7 +120,7 @@ async function handleDelete(pointUrl: string) {
await deleteImageFromS3(pinRecord);

console.log('Deleting pin record: ', pointUrl);
await dynamo.delete({ TableName: pinTable, Key: { pointUrl }})
await dynamo.delete({ TableName: pinTable, Key: { pointUrl } })
.promise();

return transformResult({ statusCode: 204 });
Expand Down
5 changes: 5 additions & 0 deletions lib/api/pin-socket-lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { APIGatewayProxyEvent, Context } from 'aws-lambda'

export function handler (event: APIGatewayProxyEvent, context: Context) {
console.log('STREAM', event)
}
2 changes: 2 additions & 0 deletions lib/shared/types/nominatim.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface GeocodeResponse {
readonly category: string;
readonly type: string;
readonly importance: number;
readonly address: any;
}

export interface ReverseGeocodeResponse {
Expand All @@ -33,4 +34,5 @@ export interface ReverseGeocodeResponse {
readonly importance: number;
readonly addresstype: string;
readonly name: string;
readonly address: any;
}
1 change: 1 addition & 0 deletions lib/shared/types/pin.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type PinPoint = {
}

export interface Pin {
customName: string;
point: PinPoint;
address?: NominatimResponse;
unsavedImage?: Image;
Expand Down
4 changes: 3 additions & 1 deletion lib/tsconfig.web.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"typeRoots": ["node_modules/@types"]
"typeRoots": ["node_modules/@types"],
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
Expand Down
27 changes: 23 additions & 4 deletions lib/web/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ import { NominatimService } from './services/nominatim.service';
import { PinState } from './state/pin.state';
import { environment } from '../environments/environment';
import { PinApiService } from './services/pin-api.service';

import { ShareDialogComponent } from './components/share-dialog.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { routes } from './routes';
import { ImagePageComponent } from './components/image.page.component';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslationBasicLoader } from './translation-loader';

@NgModule({
declarations: [
Expand All @@ -38,7 +45,9 @@ import { PinApiService } from './services/pin-api.service';
MapComponent,
PinMarkerComponent,
SearchComponent,
SidebarComponent
SidebarComponent,
ShareDialogComponent,
ImagePageComponent
],
imports: [
BrowserModule,
Expand All @@ -55,18 +64,28 @@ import { PinApiService } from './services/pin-api.service';
MatInputModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatDialogModule,
MatSelectModule,
NgxsModule.forRoot([PinState], {
developmentMode: !environment.production
}),
RouterModule.forRoot(routes),
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: () => new TranslationBasicLoader()
}
})
],
providers: [
NominatimService,
PinApiService,
{ provide: Config, useFactory: configFactory, deps: [Meta] }
],
entryComponents: [PinMarkerComponent],
entryComponents: [PinMarkerComponent, ShareDialogComponent],
bootstrap: [AppComponent]
})
export class AppModule {

}
23 changes: 11 additions & 12 deletions lib/web/src/app/components/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,19 @@ import { Pin } from 'shared/types/pin.types';
}
`],
template: `
<div class="header">
<app-header [pin]="selectedPin$ | async"></app-header>
<div class="header">
<app-header [pin]="selectedPin$ | async"></app-header>
</div>
<div class="main">
<div class="fixed">
<router-outlet></router-outlet>
</div>
<div class="main">
<div class="fixed">
<div map [pin]="selectedPin$ | async"></div>
</div>
<div class="sidebar" *ngIf="selectedPin$ | async">
<app-sidebar [pin]="selectedPin$ | async"></app-sidebar>
</div>
</div>`
<div class="sidebar" *ngIf="selectedPin$ | async">
<app-sidebar [pin]="selectedPin$ | async"></app-sidebar>
</div>
</div>`
})
export class AppComponent {

@Select(PinState.selectedPin) selectedPin$: Observable<Pin>;

}
Loading