diff --git a/capitulo_5/5.9.1/test/handler.test.js b/capitulo_5/5.9.1/test/handler.test.js
index f4b9182..af5938e 100644
--- a/capitulo_5/5.9.1/test/handler.test.js
+++ b/capitulo_5/5.9.1/test/handler.test.js
@@ -13,7 +13,7 @@ const event = {
it('#redirect', async () => {
const result = await redirect(event)
expect(result.statusCode).toBe(301)
- expect(result.headers).toEqual({ Location: 'https://novatec.com.br/livros/nodejs-3ed/' })
+ expect(result.headers).toEqual({ Location: 'https://www.novatec.com.br/livros/nodejs-3ed/' })
expect(typeof result.body).toBe('string')
})
it('#redirect link non existent', async () => {
diff --git a/capitulo_5/5.9.2/.nvmrc b/capitulo_5/5.9.2/.nvmrc
new file mode 100644
index 0000000..3c03207
--- /dev/null
+++ b/capitulo_5/5.9.2/.nvmrc
@@ -0,0 +1 @@
+18
diff --git a/capitulo_5/5.9.2/jest.config.js b/capitulo_5/5.9.2/jest.config.js
new file mode 100644
index 0000000..b9f744d
--- /dev/null
+++ b/capitulo_5/5.9.2/jest.config.js
@@ -0,0 +1,7 @@
+/** @type {import('jest').Config} */
+const config = {
+ testEnvironment: 'jest-environment-node',
+ verbose: true,
+ setupFiles: ['./test/setup.js']
+}
+module.exports = config
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/package.json b/capitulo_5/5.9.2/package.json
new file mode 100644
index 0000000..46a599c
--- /dev/null
+++ b/capitulo_5/5.9.2/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "5.9.1",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "export DEBUG=wbruno:*; npx sls dynamodb install --stage local --region sa-east-1; npx sls offline start --stage local --region sa-east-1",
+ "test": "jest --coverage"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@aws-sdk/client-dynamodb": "3.345.0",
+ "@aws-sdk/lib-dynamodb": "3.345.0",
+ "debug": "4.3.4",
+ "nanoid": "3.3.6"
+ },
+ "devDependencies": {
+ "aws-sdk-client-mock": "2.1.1",
+ "dotenv": "16.1.3",
+ "jest": "29.5.0",
+ "serverless": "3.32.2",
+ "serverless-domain-manager": "7.0.4",
+ "serverless-dynamodb-local": "0.2.40",
+ "serverless-offline": "12.0.4"
+ }
+}
diff --git a/capitulo_5/5.9.2/resources/db/shorts.json b/capitulo_5/5.9.2/resources/db/shorts.json
new file mode 100644
index 0000000..f7fc7fe
--- /dev/null
+++ b/capitulo_5/5.9.2/resources/db/shorts.json
@@ -0,0 +1,10 @@
+[
+ {
+ "id": "iNxun1bg5Y",
+ "link": "https://www.novatec.com.br/livros/nodejs-3ed/"
+ },
+ {
+ "id": "typescript",
+ "link": "https://www.novatec.com.br/livros/aprendendo-typescript/"
+ }
+]
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/resources/serverless.local.yml b/capitulo_5/5.9.2/resources/serverless.local.yml
new file mode 100644
index 0000000..224ac09
--- /dev/null
+++ b/capitulo_5/5.9.2/resources/serverless.local.yml
@@ -0,0 +1,23 @@
+provider:
+ vpc:
+ securityGroupIds:
+ - sg-..
+ subnetIds:
+ - subnet-..
+ iam:
+ role:
+ name: serverless-${self:service}-${self:provider.stage}-role
+resources:
+ Resources: ${file(resources/table.yml)}
+apiGateway:
+ apiKeys:
+ - name: localKey
+ value: d41d8cd98f00b204e9800998ecf8427e
+custom:
+ customDomain:
+ domainName: localhost
+environment:
+ AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
+ AWS_REGION: sa-east-1
+ NODE_ENV: development
+ DYNAMODB_ENDPOINT: http://localhost:8000
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/resources/table.yml b/capitulo_5/5.9.2/resources/table.yml
new file mode 100644
index 0000000..96e028c
--- /dev/null
+++ b/capitulo_5/5.9.2/resources/table.yml
@@ -0,0 +1,11 @@
+shortenerTable:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: Shortener
+ AttributeDefinitions:
+ - AttributeName: id
+ AttributeType: S
+ KeySchema:
+ - AttributeName: id
+ KeyType: HASH
+ BillingMode: PAY_PER_REQUEST
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/serverless.yml b/capitulo_5/5.9.2/serverless.yml
new file mode 100644
index 0000000..7dec814
--- /dev/null
+++ b/capitulo_5/5.9.2/serverless.yml
@@ -0,0 +1,116 @@
+service: shortener
+plugins:
+ - serverless-domain-manager
+ - serverless-dynamodb-local
+ - serverless-offline #needs to be last in the list
+useDotenv: true
+configValidationMode: warn
+package:
+ individually: false
+ excludeDevDependencies: true
+ patterns:
+ - node_modules/**
+ - src/**
+provider:
+ name: aws
+ architecture: arm64
+ region: ${opt:region}
+ stage: ${opt:stage}
+ runtime: nodejs18.x
+ versionFunctions: true
+ logRetentionInDays: 3
+ timeout: 30
+ stackName: serverless-${self:service}-${self:provider.stage}
+ memorySize: 128
+ tracing:
+ apiGateway: true
+ apiGateway:
+ apiKeys: ${file(./resources/serverless.${self:provider.stage}.yml):apiGateway.apiKeys, ''}
+ apiKeySourceType: HEADER
+ disableDefaultEndpoint: true
+ stackTags:
+ Name: ${self:service}
+ deploymentBucket:
+ name: algum-s3-${self:provider.stage}-deployment
+ environment: ${file(./resources/serverless.${self:provider.stage}.yml):environment}
+ iam: ${file(./resources/serverless.${self:provider.stage}.yml):provider.iam, ''}
+ logs:
+ restApi:
+ accessLogging: false
+ executionLogging: false
+functions:
+ redirect:
+ description: redirects hash to given url
+ handler: src/handler.redirect
+ events:
+ - http:
+ path: /{id}
+ method: get
+ cors: true
+ private: false
+ byId:
+ description: retrieve a shortened url by its id
+ handler: src/shortener.byId
+ events:
+ - http:
+ path: /shorts/{id}
+ method: get
+ cors: true
+ private: true
+ create:
+ description: create a new shortened url
+ handler: src/shortener.create
+ events:
+ - http:
+ path: /shorts
+ method: post
+ cors: true
+ private: true
+ list:
+ description: list all shortened urls
+ handler: src/shortener.list
+ events:
+ - http:
+ path: /shorts
+ method: get
+ cors: true
+ private: true
+ update:
+ description: update a new shortened url
+ handler: src/shortener.update
+ events:
+ - http:
+ path: /shorts/{id}
+ method: put
+ cors: true
+ private: true
+ remove:
+ description: remove a new shortened url
+ handler: src/shortener.remove
+ events:
+ - http:
+ path: /shorts/{id}
+ method: delete
+ cors: true
+ private: true
+resources: ${file(./resources/serverless.${self:provider.stage}.yml):resources, ''}
+custom:
+ dynamodb:
+ stages:
+ - local
+ start:
+ seed: true
+ migrate: true
+ seed:
+ domain:
+ sources:
+ - table: Shortener
+ sources: [./resources/db/shorts.json]
+ endpoints:
+ dynamodb-url: 'http://localhost:8000'
+ customDomain:
+ enabled: true
+ domainName: ${self:service}.${self:provider.stage}.seudominio.com.br
+ stage: ${self:provider.stage}
+ createRoute53Record: false
+ basePath: ''
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/src/config/dynamodb.js b/capitulo_5/5.9.2/src/config/dynamodb.js
new file mode 100644
index 0000000..d1734b3
--- /dev/null
+++ b/capitulo_5/5.9.2/src/config/dynamodb.js
@@ -0,0 +1,15 @@
+const { DynamoDBClient } = require('@aws-sdk/client-dynamodb')
+const { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb')
+
+const marshallOptions = {
+ removeUndefinedValues: true
+}
+
+const localConfig = { endpoint: process.env.DYNAMODB_ENDPOINT }
+/* istanbul ignore next */
+const config = process.env.DYNAMODB_ENDPOINT ? localConfig : {}
+
+const dynamodb = new DynamoDBClient(config)
+const documentClient = DynamoDBDocumentClient.from(dynamodb, { marshallOptions })
+
+module.exports = { dynamodb, documentClient }
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/src/handler.js b/capitulo_5/5.9.2/src/handler.js
new file mode 100644
index 0000000..aeba855
--- /dev/null
+++ b/capitulo_5/5.9.2/src/handler.js
@@ -0,0 +1,27 @@
+const html = `
+
301 Moved Permanently
+
+301 Moved Permanently
+
+`
+const repository = require('./repository')
+const redirect = (event) => {
+ const params = event.pathParameters
+
+ return repository.byId(params.id)
+ .then(result => {
+ console.log({ result })
+ if (!result?.link) {
+ throw new Error('not found')
+ }
+ return result
+ })
+ .then(result => {
+ const headers = { Location: result.link }
+ return { statusCode: 301, headers, body: html }
+ })
+ .catch((err) => {
+ return { statusCode: 404, body: JSON.stringify(err) }
+ })
+}
+module.exports = { redirect }
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/src/repository.js b/capitulo_5/5.9.2/src/repository.js
new file mode 100644
index 0000000..c7ba23b
--- /dev/null
+++ b/capitulo_5/5.9.2/src/repository.js
@@ -0,0 +1,74 @@
+const { ScanCommand, GetCommand, PutCommand, DeleteCommand } = require('@aws-sdk/lib-dynamodb')
+const debug = require('debug')('wbruno:repository')
+const { nanoid } = require('nanoid')
+const { documentClient } = require('./config/dynamodb')
+const getNextLink = (data) => {
+ if (data?.LastEvaluatedKey) {
+ const urlParams = qs.stringify(data.LastEvaluatedKey)
+ return {
+ rel: 'next',
+ href: `?${urlParams}`
+ }
+ }
+ return {}
+}
+const repository = {
+ DEFAULT_SIZE: '50',
+ TABLE_NAME: 'Shortener',
+
+ list(query, sizeParam) {
+ const size = parseInt(sizeParam || this.DEFAULT_SIZE, 10)
+
+ const config = {
+ TableName: this.TABLE_NAME,
+ Limit: size
+ }
+
+ if (Reflect.has(query, 'id')) {
+ config.ExclusiveStartKey = { id: query.id }
+ }
+ debug({ config })
+
+ const params = new ScanCommand(config)
+ return documentClient.send(params).then((result) => {
+ return {
+ size,
+ links: [getNextLink(result)],
+ items: result.Items || []
+ }
+ })
+ },
+
+ byId(id) {
+ debug({ id })
+ const params = new GetCommand({
+ Key: { id },
+ TableName: this.TABLE_NAME
+ })
+ return documentClient.send(params).then((result) => result.Item)
+ },
+
+ put(data, existendId) {
+ const id = existendId || nanoid()
+ const params = new PutCommand({
+ TableName: this.TABLE_NAME,
+ Item: {
+ id,
+ ...data
+ },
+ ReturnValues: 'ALL_OLD'
+ })
+
+ return documentClient.send(params)
+ },
+
+ delete(id) {
+ const params = new DeleteCommand({
+ TableName: this.TABLE_NAME,
+ Key: { id }
+ })
+
+ return documentClient.send(params).then((_) => ({}))
+ }
+}
+module.exports = repository
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/src/shortener.js b/capitulo_5/5.9.2/src/shortener.js
new file mode 100644
index 0000000..b408d69
--- /dev/null
+++ b/capitulo_5/5.9.2/src/shortener.js
@@ -0,0 +1,71 @@
+const debug = require('debug')('wbruno:shortener')
+const repository = require('./repository')
+
+const headers = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Credentials': true
+}
+
+const list = (event) => {
+ const query = event.queryStringParameters || {}
+
+ return repository.list(query, query.size).then((result) => {
+ return { statusCode: 200, headers, body: JSON.stringify(result) }
+ })
+}
+
+const byId = (event) => {
+ const params = event.pathParameters
+
+ return repository
+ .byId(params.id)
+ .then((result) => {
+ if (!result?.link) {
+ throw new Error('not found')
+ }
+ return result
+ })
+ .then((result) => {
+ return { statusCode: 200, headers, body: JSON.stringify(result) }
+ })
+ .catch((err) => {
+ return { statusCode: 404, body: JSON.stringify(err) }
+ })
+}
+
+const create = (event) => {
+ const body = JSON.parse(event.body || '{}')
+
+ debug('create', body)
+
+ return repository.put(body).then((_) => {
+ const result = { message: 'created' }
+ return { statusCode: 201, headers, body: JSON.stringify(result) }
+ })
+}
+
+const update = (event) => {
+ const params = event.pathParameters
+ const body = JSON.parse(event.body || '{}')
+
+ debug('update', body)
+
+ return repository.put(body, params.id).then((_) => {
+ const result = { message: 'updated' }
+ return { statusCode: 200, headers, body: JSON.stringify(result) }
+ })
+}
+
+const remove = (event) => {
+ const params = event.pathParameters
+
+ debug('delete', params.id)
+
+ return repository.delete(params.id).then(() => {
+ return { statusCode: 200, headers, body: '' }
+ })
+}
+
+module.exports = {
+ list, byId, create, update, remove
+}
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/test/handler.test.js b/capitulo_5/5.9.2/test/handler.test.js
new file mode 100644
index 0000000..af5938e
--- /dev/null
+++ b/capitulo_5/5.9.2/test/handler.test.js
@@ -0,0 +1,23 @@
+const { redirect } = require('../src/handler')
+const event = {
+ "resource": "/{id}",
+ "path": "/iNxun1bg5Y",
+ "httpMethod": "GET",
+ "pathParameters": {
+ "id": "iNxun1bg5Y"
+ },
+ "body": null,
+ "isBase64Encoded": false
+}
+
+it('#redirect', async () => {
+ const result = await redirect(event)
+ expect(result.statusCode).toBe(301)
+ expect(result.headers).toEqual({ Location: 'https://www.novatec.com.br/livros/nodejs-3ed/' })
+ expect(typeof result.body).toBe('string')
+})
+it('#redirect link non existent', async () => {
+ const result = await redirect({ pathParameters: { id: 'non-existent' }})
+ expect(result.statusCode).toBe(404)
+ expect(result.body).toBe('{}')
+})
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/test/setup.js b/capitulo_5/5.9.2/test/setup.js
new file mode 100644
index 0000000..54a162a
--- /dev/null
+++ b/capitulo_5/5.9.2/test/setup.js
@@ -0,0 +1,24 @@
+require('dotenv').config()
+const { mockClient } = require('aws-sdk-client-mock')
+const { DynamoDBDocumentClient, GetCommand, QueryCommand, PutCommand, ScanCommand, DeleteCommand } = require ('@aws-sdk/lib-dynamodb')
+const ddbMock = mockClient(DynamoDBDocumentClient)
+const mockData = {
+ link: 'https://www.novatec.com.br/livros/nodejs-3ed/'
+}
+ddbMock
+ .on(GetCommand)
+ .resolves({ Item: undefined })
+ .on(GetCommand, {
+ TableName: 'Shortener',
+ Key: { id: 'iNxun1bg5Y' }
+ })
+ .resolves({ Item: mockData })
+
+ddbMock
+ .on(QueryCommand)
+ .resolves({ Items: [mockData] })
+
+ddbMock
+ .on(PutCommand).resolves({})
+ .on(DeleteCommand).resolves({})
+ddbMock.on(ScanCommand).resolves({ Items: [mockData] })
\ No newline at end of file
diff --git a/capitulo_5/5.9.2/test/shortener.test.js b/capitulo_5/5.9.2/test/shortener.test.js
new file mode 100644
index 0000000..93f7078
--- /dev/null
+++ b/capitulo_5/5.9.2/test/shortener.test.js
@@ -0,0 +1,69 @@
+const { byId, create, remove, list, update } = require('../src/shortener')
+
+it('#byId', async () => {
+ const result = await byId({
+ headers: {},
+ pathParameters: { id: 'iNxun1bg5Y' }
+ })
+ const expected = {
+ link: 'https://www.novatec.com.br/livros/nodejs-3ed/'
+ }
+ const body = JSON.parse(result.body || '{}')
+
+ expect(expected).toEqual(body)
+ expect(result.statusCode).toEqual(200)
+ expect(typeof result.body).toEqual('string')
+})
+
+it('#create', async () => {
+ const result = await create({
+ body: '{"link": "https://www.novatec.com.br/livros/nodejs-3ed/"}',
+ pathParameters: {}
+ })
+ const expected = {
+ message: 'created'
+ }
+ const body = JSON.parse(result.body || '{}')
+
+ expect(expected).toEqual(body)
+ expect(201).toEqual(result.statusCode)
+ expect(typeof result.body).toEqual('string')
+})
+it('#list', async () => {
+ const result = await list({ headers: {} })
+ const expected = {
+ size: 50,
+ links: [{}],
+ items: [{ link: 'https://www.novatec.com.br/livros/nodejs-3ed/' }]
+ }
+ const body = JSON.parse(result.body || '{}')
+
+ expect(body).toEqual(expected)
+ expect(result.statusCode).toEqual(200)
+ expect(typeof result.body).toEqual('string')
+})
+it('#remove', async () => {
+ const result = await remove({
+ headers: {},
+ pathParameters: { id: 'iNxun1bg5Y' }
+ })
+
+ expect(result.body).toEqual('')
+ expect(result.statusCode).toEqual(200)
+ expect(typeof result.body).toEqual('string')
+})
+
+it('#update', async () => {
+ const result = await update({
+ body: '{"link": "https://www.novatec.com.br/livros/nodejs-3ed/"}',
+ pathParameters: { id: 'iNxun1bg5Y' }
+ })
+ const expected = {
+ message: 'updated'
+ }
+ const body = JSON.parse(result.body || '{}')
+
+ expect(expected).toEqual(body)
+ expect(200).toEqual(result.statusCode)
+ expect(typeof result.body).toEqual('string')
+})
\ No newline at end of file