diff --git a/.github/workflows/run-test-and-publish.yaml b/.github/workflows/run-test-and-publish.yaml index 08089ed..aa189fd 100644 --- a/.github/workflows/run-test-and-publish.yaml +++ b/.github/workflows/run-test-and-publish.yaml @@ -26,6 +26,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Build packages + run: npm run build + - name: Run Unit Tests run: npm run test diff --git a/.github/workflows/run-test.yaml b/.github/workflows/run-test.yaml index 9301b76..5aaffb4 100644 --- a/.github/workflows/run-test.yaml +++ b/.github/workflows/run-test.yaml @@ -23,6 +23,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Build packages + run: npm run build + - name: Run Unit Tests run: npm run test diff --git a/README.md b/README.md index 288e67c..b032701 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png) +![MBC CQRS serverless framework](https://mbc-cqrs-serverless.mbc-net.com/img/mbc-cqrs-serverless.png) # MBC CQRS serverless framework @@ -10,7 +10,7 @@ This package provides core functionalities for implementing the Command Query Re ## Documentation -Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation. +Visit https://mbc-cqrs-serverless.mbc-net.com/ to view the full documentation. ## Features @@ -95,5 +95,6 @@ $ npm run release - Serverless framework: https://www.serverless.com/framework/docs ## License + Copyright © 2024, Murakami Business Consulting, Inc. https://www.mbc-net.com/ This project and sub projects are under the MIT License. diff --git a/lerna.json b/lerna.json index 7d2434b..3fd5127 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "packages": ["packages/*"] } diff --git a/package-lock.json b/package-lock.json index a626437..c98d2af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36035,7 +36035,7 @@ }, "packages/cli": { "name": "@mbc-cqrs-serverless/cli", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "license": "MIT", "dependencies": { "commander": "^11.1.0", @@ -36045,8 +36045,7 @@ "mbc": "dist/index.js" }, "devDependencies": { - "@faker-js/faker": "^8.3.1", - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@faker-js/faker": "^8.3.1" } }, "packages/cli/node_modules/commander": { @@ -36076,7 +36075,7 @@ }, "packages/core": { "name": "@mbc-cqrs-serverless/core", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "license": "MIT", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", @@ -36165,26 +36164,26 @@ }, "packages/sequence": { "name": "@mbc-cqrs-serverless/sequence", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "license": "MIT", "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.30-beta.0" } }, "packages/task": { "name": "@mbc-cqrs-serverless/task", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "license": "MIT", "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.30-beta.0" } }, "packages/ui-setting": { "name": "@mbc-cqrs-serverless/ui-setting", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "license": "MIT", "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.30-beta.0" } } } diff --git a/package.json b/package.json index 0e08926..cf5f5d0 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "bugs": { "url": "https://github.com/mbc-net/mbc-cqrs-serverless/issues" }, - "homepage": "https://mbc-net.github.io/mbc-cqrs-serverless-doc/", + "homepage": "https://mbc-cqrs-serverless.mbc-net.com/", "devDependencies": { "@aws-sdk/client-dynamodb": "^3.478.0", "@aws-sdk/client-s3": "^3.478.0", diff --git a/packages/cli/README.md b/packages/cli/README.md index 56f4e2f..09032d9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,10 +1,39 @@ -![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png) +![MBC CQRS serverless framework](https://mbc-cqrs-serverless.mbc-net.com/img/mbc-cqrs-serverless.png) + # MBC CQRS serverless framework CLI package +## Description + +The MBC CLI is a command-line interface tool that helps you to initialize your mbc-cqrs-serverless applications. + +## Installation + +To install `mbc`, run: + +```bash +npm install -g @mbc-cqrs-serverless/cli +``` + +## Usage + +### `mbc new|n [projectName@version]` + +There are 3 usages for the new command: + +- `mbc new` + - Creates a new project in the current folder using a default name with the latest framework version. +- `mbc new [projectName]` + - Creates a new project in the `projectName` folder using the latest framework version. +- `mbc new [projectName@version]` + - If the specified version exists, the CLI uses that exact version. + - If the provided version is a prefix, the CLI uses the latest version matching that prefix. + - If no matching version is found, the CLI logs an error and provides a list of available versions for the user. + ## Documentation -Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation. +Visit https://mbc-cqrs-serverless.mbc-net.com/ to view the full documentation. ## License + Copyright © 2024, Murakami Business Consulting, Inc. https://www.mbc-net.com/ This project and sub projects are under the MIT License. diff --git a/packages/cli/package.json b/packages/cli/package.json index 49aee1c..91e02c4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/cli", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "description": "a CLI to get started with MBC CQRS serverless framework", "keywords": [ "mbc", @@ -38,7 +38,7 @@ "bugs": { "url": "https://github.com/mbc-net/mbc-cqrs-serverless/issues" }, - "homepage": "https://mbc-net.github.io/mbc-cqrs-serverless-doc/", + "homepage": "https://mbc-cqrs-serverless.mbc-net.com/", "publishConfig": { "access": "public" }, @@ -47,7 +47,6 @@ "rimraf": "^5.0.5" }, "devDependencies": { - "@faker-js/faker": "^8.3.1", - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@faker-js/faker": "^8.3.1" } } diff --git a/packages/cli/src/actions/new.action.get-package-version.spec.ts b/packages/cli/src/actions/new.action.get-package-version.spec.ts new file mode 100644 index 0000000..9cee577 --- /dev/null +++ b/packages/cli/src/actions/new.action.get-package-version.spec.ts @@ -0,0 +1,42 @@ +import { execSync } from 'child_process' + +import { exportsForTesting } from './new.action' + +const { getPackageVersion } = exportsForTesting + +jest.mock('child_process', () => ({ + execSync: jest.fn(), +})) + +describe('getPackageVersion', () => { + const mockExecSync = execSync as jest.MockedFunction + const packageName = '@mbc-cqrs-serverless/core' + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should return the latest version when isLatest is true', () => { + const mockLatestVersion = '1.2.3' + mockExecSync.mockReturnValue(Buffer.from(`${mockLatestVersion}\n`)) + + const result = getPackageVersion(packageName, true) + + expect(mockExecSync).toHaveBeenCalledWith( + `npm view ${packageName} dist-tags.latest`, + ) + expect(result).toEqual([mockLatestVersion]) + }) + + it('should return all versions when isLatest is false', () => { + const mockVersions = ['1.0.0', '1.1.0', '1.2.0'] + mockExecSync.mockReturnValue(Buffer.from(JSON.stringify(mockVersions))) + + const result = getPackageVersion(packageName, false) + + expect(mockExecSync).toHaveBeenCalledWith( + `npm view ${packageName} versions --json`, + ) + expect(result).toEqual(mockVersions) + }) +}) diff --git a/packages/cli/src/actions/new.action.is-latest-version.spec.ts b/packages/cli/src/actions/new.action.is-latest-version.spec.ts new file mode 100644 index 0000000..4af88d4 --- /dev/null +++ b/packages/cli/src/actions/new.action.is-latest-version.spec.ts @@ -0,0 +1,46 @@ +import { execSync } from 'child_process' + +import { exportsForTesting } from './new.action' + +const { isLatestCli } = exportsForTesting + +jest.mock('child_process', () => ({ + execSync: jest.fn(), +})) + +jest.mock('fs', () => ({ + readFileSync: jest.fn(() => JSON.stringify({ version: '1.0.0' })), +})) +jest.mock('path', () => ({ + join: jest.fn(() => '/mocked/path/to/package.json'), +})) + +describe('isLatestCli', () => { + const mockExecSync = execSync as jest.MockedFunction + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should return true if the current version matches the latest version', () => { + const mockLatestVersion = ['1.0.0'] + mockExecSync.mockReturnValue(Buffer.from(`${mockLatestVersion}\n`)) + + // Run the function + const result = isLatestCli() + + // Assert that the result is true + expect(result).toBe(true) + }) + + it('should return false if the current version does not match the latest version', () => { + const mockLatestVersion = ['1.2.3'] + mockExecSync.mockReturnValue(Buffer.from(`${mockLatestVersion}\n`)) + + // Run the function + const result = isLatestCli() + + // Assert that the result is true + expect(result).toBe(false) + }) +}) diff --git a/packages/cli/src/actions/new.action.spec.ts b/packages/cli/src/actions/new.action.spec.ts index 923f5b0..3e08cac 100644 --- a/packages/cli/src/actions/new.action.spec.ts +++ b/packages/cli/src/actions/new.action.spec.ts @@ -1,61 +1,113 @@ -import { faker } from '@faker-js/faker' -import { copyFileSync, readFileSync, unlinkSync } from 'fs' +import { execSync } from 'child_process' +import { Command } from 'commander' +import { copyFileSync, cpSync, mkdirSync } from 'fs' import path from 'path' -import { exportsForTesting } from './new.action' +import newAction, { exportsForTesting } from './new.action' -const { useLatestPackageVersion } = exportsForTesting +jest.mock('child_process', () => ({ + execSync: jest.fn(), +})) -// create testcase for useLatestPackageVersion function in new.action.ts file -describe('useLatestPackageVersion', () => { - const fname = path.join(__dirname, 'package.json') - const packageJson = JSON.parse( - readFileSync(path.join(__dirname, '../../package.json')).toString(), - ) +jest.mock('fs', () => ({ + copyFileSync: jest.fn(), + cpSync: jest.fn(), + mkdirSync: jest.fn(), + unlinkSync: jest.fn(), + writeFileSync: jest.fn(), + readFileSync: jest.fn(() => + JSON.stringify({ version: '1.2.3', dependencies: {}, devDependencies: {} }), + ), +})) + +describe('newAction', () => { + const mockExecSync = execSync as jest.MockedFunction + const mockCommand = new Command().name('new') // Mock command with name 'new' beforeEach(() => { - copyFileSync(path.join(__dirname, '../../templates/package.json'), fname) + jest.clearAllMocks() }) - afterEach(() => { - unlinkSync(fname) - }) + it('should generate a project with the latest version when version is not specified', async () => { + const projectName = 'test-project' + const latestVersion = '1.2.3' + mockExecSync + .mockReturnValueOnce(Buffer.from(latestVersion)) // latest core + .mockReturnValueOnce(Buffer.from(latestVersion)) // latest cli + .mockReturnValue(Buffer.from('')) - it('it should update deps', () => { - useLatestPackageVersion(__dirname) - const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + await newAction(`${projectName}`, {}, mockCommand) - expect(packageJson.devDependencies['@mbc-cqrs-serverless/core']).toBe( - tplPackageJson.dependencies['@mbc-cqrs-serverless/core'], + expect(execSync).toHaveBeenCalledWith( + 'npm view @mbc-cqrs-serverless/core dist-tags.latest', + ) + expect(mkdirSync).toHaveBeenCalledWith( + path.join(process.cwd(), projectName), + { recursive: true }, ) - expect(packageJson.version).toBe( - tplPackageJson.devDependencies['@mbc-cqrs-serverless/cli'], + expect(cpSync).toHaveBeenCalledWith( + path.join(__dirname, '../../templates'), + path.join(process.cwd(), projectName), + { recursive: true }, ) + expect(copyFileSync).toHaveBeenCalledTimes(3) // For .gitignore, infra/.gitignore and .env.local + expect(mockExecSync).toHaveBeenCalledWith('git init', { + cwd: path.join(process.cwd(), projectName), + }) + expect(mockExecSync).toHaveBeenCalledWith('npm i', { + cwd: path.join(process.cwd(), projectName), + }) }) - it('it should not update name', () => { - const { name } = JSON.parse(readFileSync(fname).toString()) + it('should use a specific version if specified', async () => { + const projectName = 'test-project' + const version = '1.0.0' + const mockVersions = ['1.0.0', '1.1.0', '1.2.0'] + mockExecSync + .mockReturnValueOnce(Buffer.from(JSON.stringify(mockVersions))) // list version core + .mockReturnValueOnce(Buffer.from('1.2.3')) // latest cli + .mockReturnValue(Buffer.from('')) - useLatestPackageVersion(__dirname) - const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + await newAction(`${projectName}@${version}`, {}, mockCommand) - expect(name).toBe(tplPackageJson.name) + expect(execSync).toHaveBeenCalledWith( + 'npm view @mbc-cqrs-serverless/core versions --json', + ) + expect(mkdirSync).toHaveBeenCalledWith( + path.join(process.cwd(), projectName), + { recursive: true }, + ) + expect(cpSync).toHaveBeenCalledWith( + path.join(__dirname, '../../templates'), + path.join(process.cwd(), projectName), + { recursive: true }, + ) + expect(copyFileSync).toHaveBeenCalledTimes(3) // For .gitignore, infra/.gitignore and .env.local + expect(mockExecSync).toHaveBeenCalledWith('git init', { + cwd: path.join(process.cwd(), projectName), + }) + expect(mockExecSync).toHaveBeenCalledWith('npm i', { + cwd: path.join(process.cwd(), projectName), + }) }) - it('it should not update name with empty name', () => { - const { name } = JSON.parse(readFileSync(fname).toString()) - - useLatestPackageVersion(__dirname, '') - const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + it('should throw an error for an invalid version', async () => { + const projectName = 'test-project' + const invalidVersion = '2.0.0' + const mockVersions = ['1.0.0', '1.1.0', '1.2.0'] + mockExecSync.mockReturnValueOnce(Buffer.from(JSON.stringify(mockVersions))) - expect(name).toBe(tplPackageJson.name) - }) + const consoleSpy = jest.spyOn(console, 'log').mockImplementation() - it('it should update name', () => { - const name = faker.word.sample() - useLatestPackageVersion(__dirname, name) - const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + await newAction(`${projectName}@${invalidVersion}`, {}, mockCommand) - expect(name).toBe(tplPackageJson.name) + expect(execSync).toHaveBeenCalledWith( + 'npm view @mbc-cqrs-serverless/core versions --json', + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'The specified package version does not exist. Please chose a valid version!\n', + mockVersions, + ) + consoleSpy.mockRestore() }) }) diff --git a/packages/cli/src/actions/new.action.ts b/packages/cli/src/actions/new.action.ts index 6450039..c405e3f 100644 --- a/packages/cli/src/actions/new.action.ts +++ b/packages/cli/src/actions/new.action.ts @@ -5,6 +5,7 @@ import { cpSync, mkdirSync, readFileSync, + rmSync, unlinkSync, writeFileSync, } from 'fs' @@ -16,24 +17,52 @@ export default async function newAction( options: object, command: Command, ) { + const [projectName, version = 'latest'] = name.split('@') + console.log( - `Executing command '${command.name()}' for application '${name}' with options '${JSON.stringify( + `Executing command '${command.name()}' for application '${projectName}' with options '${JSON.stringify( options, )}'`, ) - const destDir = path.join(process.cwd(), name) + let packageVersion + + if (version === 'latest') { + packageVersion = `^${ + getPackageVersion('@mbc-cqrs-serverless/core', true)[0] + }` // use the latest patch and minor versions + } else { + const versions = getPackageVersion('@mbc-cqrs-serverless/core') + const regex = new RegExp(`^${version}(?![0-9]).*$`) // start with version and not directly follow by a digit + const matchVersions = versions.filter((v) => regex.test(v)) + if (versions.includes(version)) { + packageVersion = version // specific version + } else if (matchVersions.length !== 0) { + packageVersion = `^${matchVersions.at(-1)}` // use the patch and minor versions + } else { + console.log( + 'The specified package version does not exist. Please chose a valid version!\n', + versions, + ) + return + } + } + + const destDir = path.join(process.cwd(), projectName) console.log('Generating MBC cqrs serverless application in', destDir) mkdirSync(destDir, { recursive: true }) - cpSync(path.join(__dirname, '../../templates'), destDir, { recursive: true }) + useTemplate(destDir) - // upgrade package - useLatestPackageVersion(destDir, name) + usePackageVersion(destDir, packageVersion, projectName) // mv gitignore .gitignore const gitignore = path.join(destDir, 'gitignore') copyFileSync(gitignore, path.join(destDir, '.gitignore')) unlinkSync(gitignore) + // mv infra/gitignore infra/.gitignore + const infraGitignore = path.join(destDir, 'infra/gitignore') + copyFileSync(infraGitignore, path.join(destDir, 'infra/.gitignore')) + unlinkSync(infraGitignore) // cp .env.local .env copyFileSync(path.join(destDir, '.env.local'), path.join(destDir, '.env')) @@ -47,27 +76,73 @@ export default async function newAction( console.log(logs.toString()) } -function useLatestPackageVersion(destDir: string, name?: string) { +function useTemplate(destDir: string) { + if (isLatestCli()) { + cpSync(path.join(__dirname, '../../templates'), destDir, { + recursive: true, + }) + } else { + execSync('npm i @mbc-cqrs-serverless/cli', { cwd: destDir }) + cpSync( + path.join(destDir, 'node_modules/@mbc-cqrs-serverless/cli/templates'), + destDir, + { recursive: true }, + ) + rmSync(path.join(destDir, 'node_modules'), { + recursive: true, + }) + } +} + +function isLatestCli() { + const latestVersion = getPackageVersion('@mbc-cqrs-serverless/cli', true)[0] + const packageJson = JSON.parse( + readFileSync(path.join(__dirname, '../../package.json')).toString(), + ) + const curVersion = packageJson.version + return latestVersion === curVersion +} + +function usePackageVersion( + destDir: string, + packageVersion: string, + projectName?: string, +) { const packageJson = JSON.parse( readFileSync(path.join(__dirname, '../../package.json')).toString(), ) const fname = path.join(destDir, 'package.json') const tplPackageJson = JSON.parse(readFileSync(fname).toString()) - if (name) { - tplPackageJson.name = name + if (projectName) { + tplPackageJson.name = projectName } - tplPackageJson.dependencies['@mbc-cqrs-serverless/core'] = - packageJson.devDependencies['@mbc-cqrs-serverless/core'] + tplPackageJson.dependencies['@mbc-cqrs-serverless/core'] = packageVersion tplPackageJson.devDependencies['@mbc-cqrs-serverless/cli'] = packageJson.version writeFileSync(fname, JSON.stringify(tplPackageJson, null, 2)) } +function getPackageVersion(packageName: string, isLatest = false): string[] { + if (isLatest) { + const latestVersion = execSync(`npm view ${packageName} dist-tags.latest`) + .toString() + .trim() + return [latestVersion] + } + + const versions = JSON.parse( + execSync(`npm view ${packageName} versions --json`).toString(), + ) as string[] + return versions +} + export let exportsForTesting = { - useLatestPackageVersion, + usePackageVersion, + getPackageVersion, + isLatestCli, } if (process.env.NODE_ENV !== 'test') { exportsForTesting = undefined diff --git a/packages/cli/src/actions/new.action.use-package-version.spec.ts b/packages/cli/src/actions/new.action.use-package-version.spec.ts new file mode 100644 index 0000000..58483ad --- /dev/null +++ b/packages/cli/src/actions/new.action.use-package-version.spec.ts @@ -0,0 +1,62 @@ +import { faker } from '@faker-js/faker' +import { copyFileSync, readFileSync, unlinkSync } from 'fs' +import path from 'path' + +import { exportsForTesting } from './new.action' + +const { usePackageVersion } = exportsForTesting + +// create testcase for usePackageVersion function in new.action.ts file +describe('usePackageVersion', () => { + const fname = path.join(__dirname, 'package.json') + const packageVersion = '1.0.0' + const packageJson = JSON.parse( + readFileSync(path.join(__dirname, '../../package.json')).toString(), + ) + + beforeEach(() => { + copyFileSync(path.join(__dirname, '../../templates/package.json'), fname) + }) + + afterEach(() => { + unlinkSync(fname) + }) + + it('it should update deps', () => { + usePackageVersion(__dirname, packageVersion) + const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + + expect(tplPackageJson.dependencies['@mbc-cqrs-serverless/core']).toBe( + packageVersion, + ) + expect(packageJson.version).toBe( + tplPackageJson.devDependencies['@mbc-cqrs-serverless/cli'], + ) + }) + + it('it should not update name', () => { + const { name } = JSON.parse(readFileSync(fname).toString()) + + usePackageVersion(__dirname, packageVersion) + const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + + expect(name).toBe(tplPackageJson.name) + }) + + it('it should not update name with empty name', () => { + const { name } = JSON.parse(readFileSync(fname).toString()) + + usePackageVersion(__dirname, packageVersion, '') + const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + + expect(name).toBe(tplPackageJson.name) + }) + + it('it should update name', () => { + const name = faker.word.sample() + usePackageVersion(__dirname, packageVersion, name) + const tplPackageJson = JSON.parse(readFileSync(fname).toString()) + + expect(name).toBe(tplPackageJson.name) + }) +}) diff --git a/packages/cli/templates/.dockerignore b/packages/cli/templates/.dockerignore new file mode 100644 index 0000000..5b2e63f --- /dev/null +++ b/packages/cli/templates/.dockerignore @@ -0,0 +1,11 @@ +infra +infra-local +test +dist +dist_layer +node_modules +.envrc +.eslintrc.js +.prettierrc +jest.config.js +README.md diff --git a/packages/cli/templates/Dockerfile b/packages/cli/templates/Dockerfile new file mode 100644 index 0000000..155bdcf --- /dev/null +++ b/packages/cli/templates/Dockerfile @@ -0,0 +1,32 @@ +FROM node:20-bullseye-slim AS builder + +# RUN apk update +# RUN apk --no-cache add make gcc g++ --virtual .builds-deps build-base python3 musl-dev openssl-dev + +WORKDIR /app + +COPY package*.json ./ +COPY prisma ./prisma/ + +RUN npm install + +COPY . . + +RUN npm run build:prod + +FROM node:20-bullseye-slim + +# RUN apk update +# RUN apk --no-cache add make gcc g++ --virtual .builds-deps build-base python3 musl-dev openssl-dev + +WORKDIR /app + +COPY --from=builder /app/package*.json ./ +COPY prisma ./prisma/ +RUN npm ci --omit=dev + +COPY --from=builder /app/dist ./dist + +# COPY .env .env + +CMD ["npm", "run", "start:prod"] diff --git a/packages/cli/templates/README.md b/packages/cli/templates/README.md index f14f32d..68aa1b9 100644 --- a/packages/cli/templates/README.md +++ b/packages/cli/templates/README.md @@ -1,4 +1,5 @@ -![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png) +![MBC CQRS serverless framework](https://mbc-cqrs-serverless.mbc-net.com/img/mbc-cqrs-serverless.png) + ## Description MBC CQRS serverless framework based on [Nest](https://github.com/nestjs/nest). @@ -166,5 +167,5 @@ $ npm run start:repl - Ref: https://docs.nestjs.com/recipes/repl ## Documentation -Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation. +Visit https://mbc-cqrs-serverless.mbc-net.com/ to view the full documentation. diff --git a/packages/cli/templates/gitignore b/packages/cli/templates/gitignore index b99a3d2..5a625cc 100644 --- a/packages/cli/templates/gitignore +++ b/packages/cli/templates/gitignore @@ -43,4 +43,6 @@ docker-data .nestjs_repl_history -.env \ No newline at end of file +.env + +report \ No newline at end of file diff --git a/packages/cli/templates/infra-local/cognito-local/db/local_2G7noHgW.json b/packages/cli/templates/infra-local/cognito-local/db/local_2G7noHgW.json index 20ad2ce..6f60848 100644 --- a/packages/cli/templates/infra-local/cognito-local/db/local_2G7noHgW.json +++ b/packages/cli/templates/infra-local/cognito-local/db/local_2G7noHgW.json @@ -10,6 +10,18 @@ "Name": "email", "Value": "admin@test.com" }, + { + "Name": "custom:tenant_code", + "Value": "mbc" + }, + { + "Name": "custom:company_code", + "Value": "00000" + }, + { + "Name": "custom:member_id", + "Value": "MEMBER#mbc#00000#99999" + }, { "Name": "custom:roles", "Value": "[{\"role\":\"system_admin\"}]" @@ -248,6 +260,39 @@ "MinValue": "0" } }, + { + "Name": "custom:tenant_code", + "AttributeDataType": "String", + "DeveloperOnlyAttribute": false, + "Mutable": true, + "Required": false, + "StringAttributeConstraints": { + "MinLength": "0", + "MaxLength": "50" + } + }, + { + "Name": "custom:company_code", + "AttributeDataType": "String", + "DeveloperOnlyAttribute": false, + "Mutable": true, + "Required": false, + "StringAttributeConstraints": { + "MinLength": "0", + "MaxLength": "50" + } + }, + { + "Name": "custom:member_id", + "AttributeDataType": "String", + "DeveloperOnlyAttribute": false, + "Mutable": true, + "Required": false, + "StringAttributeConstraints": { + "MinLength": "0", + "MaxLength": "2048" + } + }, { "Name": "custom:roles", "AttributeDataType": "String", @@ -255,8 +300,8 @@ "Mutable": true, "Required": false, "StringAttributeConstraints": { - "MinLength": "1", - "MaxLength": "10" + "MinLength": "0", + "MaxLength": "2048" } } ], diff --git a/packages/cli/templates/infra/.cdkgraphrc.js b/packages/cli/templates/infra/.cdkgraphrc.js new file mode 100644 index 0000000..b3dc24f --- /dev/null +++ b/packages/cli/templates/infra/.cdkgraphrc.js @@ -0,0 +1,3 @@ +module.exports = { + outdir: 'graph', +} diff --git a/packages/cli/templates/infra/.prettierrc b/packages/cli/templates/infra/.prettierrc new file mode 100644 index 0000000..91f45c3 --- /dev/null +++ b/packages/cli/templates/infra/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "auto", + "tabWidth": 2, + "useTabs": false +} diff --git a/packages/cli/templates/infra/README.md b/packages/cli/templates/infra/README.md new file mode 100644 index 0000000..4ad306a --- /dev/null +++ b/packages/cli/templates/infra/README.md @@ -0,0 +1,25 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Use SSO profile + +```bash +export AWS_PROFILE= +``` + +## Useful commands + +- `npm run build` compile typescript to js +- `npm run watch` watch for changes and compile +- `npm run test` perform the jest unit tests +- `cdk deploy` deploy this stack to your default AWS account/region +- `cdk diff` compare deployed stack with current state +- `cdk synth` emits the synthesized CloudFormation template + +## Useful resources + +- https://constructs.dev/ +- https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html diff --git a/packages/cli/templates/infra/asset/schema.graphql b/packages/cli/templates/infra/asset/schema.graphql new file mode 100644 index 0000000..a544aec --- /dev/null +++ b/packages/cli/templates/infra/asset/schema.graphql @@ -0,0 +1,32 @@ +type Message @aws_api_key @aws_iam @aws_cognito_user_pools @aws_oidc { + id: String! + table: String! + pk: String! + sk: String! + tenantCode: String! + action: String! + content: AWSJSON! +} + +type Query { + getMessage(id: String!): Message +} + +type Mutation { + sendMessage(message: AWSJSON!): Message! @aws_iam +} + +type Subscription { + onMessage(tenantCode: String!, action: String, id: String): Message + @aws_subscribe(mutations: ["sendMessage"]) + @aws_api_key + @aws_iam + @aws_cognito_user_pools + @aws_oidc +} + +schema { + query: Query + mutation: Mutation + subscription: Subscription +} diff --git a/packages/cli/templates/infra/bin/infra.ts b/packages/cli/templates/infra/bin/infra.ts new file mode 100644 index 0000000..e6f7d60 --- /dev/null +++ b/packages/cli/templates/infra/bin/infra.ts @@ -0,0 +1,42 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib' +import 'source-map-support/register' +import { Env, PIPELINE_NAME } from '../config' +import { PipelineStack } from '../libs/pipeline-stack' +import { CdkGraph, FilterPreset } from '@aws/pdk/cdk-graph' +import { CdkGraphDiagramPlugin } from '@aws/pdk/cdk-graph-plugin-diagram' +;(async () => { + const app = new cdk.App() + + const cdkEnv: cdk.Environment = { + account: '', + region: '', + } + + const envs: Env[] = ['dev'] + + for (const env of envs) { + new PipelineStack(app, env + '-' + PIPELINE_NAME + '-pipeline-stack', { + env: cdkEnv, + envName: env, + }) + } + + const graph = new CdkGraph(app, { + plugins: [ + new CdkGraphDiagramPlugin({ + diagrams: [ + { + name: 'diagram', + title: 'Infrastructure diagram', + theme: 'light', + }, + ], + }), + ], + }) + + app.synth() + + await graph.report() +})() diff --git a/packages/cli/templates/infra/cdk.json b/packages/cli/templates/infra/cdk.json new file mode 100644 index 0000000..4266a03 --- /dev/null +++ b/packages/cli/templates/infra/cdk.json @@ -0,0 +1,48 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/infra.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.jsx", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true + } +} diff --git a/packages/cli/templates/infra/config/constant.ts b/packages/cli/templates/infra/config/constant.ts new file mode 100644 index 0000000..82a815d --- /dev/null +++ b/packages/cli/templates/infra/config/constant.ts @@ -0,0 +1,8 @@ +export const PIPELINE_NAME = '' +export const GIT_REPO = '' +export const GIT_CONNECTION_ARN = '' +export const ACM_HTTP_CERTIFICATE_ARN = '' // ACM_US_EAST_1 +export const ACM_APPSYNC_CERTIFICATE_ARN = '' // ACM_US_EAST_1 +export const HOSTED_ZONE_ID = '' +export const HOSTED_ZONE_NAME = '' +export const COGNITO_URL = '' diff --git a/packages/cli/templates/infra/config/dev/index.ts b/packages/cli/templates/infra/config/dev/index.ts new file mode 100644 index 0000000..cc91d55 --- /dev/null +++ b/packages/cli/templates/infra/config/dev/index.ts @@ -0,0 +1,48 @@ +import { ApplicationLogLevel, SystemLogLevel } from 'aws-cdk-lib/aws-lambda' +import { Config } from '../type' + +const config: Config = { + env: 'dev', + appName: '', + + domain: { + http: '', + appsync: '', + }, + + userPoolId: '', + + vpc: { + id: '', + subnetIds: [], + securityGroupIds: [], + }, + rds: { + accountSsmKey: '', + endpoint: '', + dbName: '', + }, + + logLevel: { + lambdaSystem: SystemLogLevel.DEBUG, + lambdaApplication: ApplicationLogLevel.TRACE, + level: 'verbose', + }, + + frontBaseUrl: '', + fromEmailAddress: '', + + // wafArn: '', + + // ecs: { + // maxInstances: 1, + // minInstances: 1, + // cpu: 512, + // memory: 1024, + // cpuThreshold: 70, + // scaleStep: 1, + // autoRollback: false, + // }, +} + +export default config diff --git a/packages/cli/templates/infra/config/index.ts b/packages/cli/templates/infra/config/index.ts new file mode 100644 index 0000000..480713e --- /dev/null +++ b/packages/cli/templates/infra/config/index.ts @@ -0,0 +1,17 @@ +import { Config, Env } from './type' +import dev from './dev' +import stg from './stg' +import prod from './prod' + +export function getConfig(env: Env): Config { + if (env === 'prod') { + return prod + } + if (env === 'stg') { + return stg + } + return dev +} + +export * from './constant' +export * from './type' diff --git a/packages/cli/templates/infra/config/prod/index.ts b/packages/cli/templates/infra/config/prod/index.ts new file mode 100644 index 0000000..64f1f25 --- /dev/null +++ b/packages/cli/templates/infra/config/prod/index.ts @@ -0,0 +1,48 @@ +import { SystemLogLevel, ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda' +import { Config } from '../type' + +const config: Config = { + env: 'prod', + appName: '', + + domain: { + http: '', + appsync: '', + }, + + userPoolId: '', + + vpc: { + id: '', + subnetIds: [], + securityGroupIds: [], + }, + rds: { + accountSsmKey: '', + endpoint: '', + dbName: '', + }, + + logLevel: { + lambdaSystem: SystemLogLevel.DEBUG, + lambdaApplication: ApplicationLogLevel.TRACE, + level: 'info', + }, + + frontBaseUrl: '', + fromEmailAddress: '', + + // wafArn: '', + + // ecs: { + // maxInstances: 2, + // minInstances: 1, + // cpu: 2048, + // memory: 4096, + // cpuThreshold: 70, + // scaleStep: 1, + // autoRollback: true, + // }, +} + +export default config diff --git a/packages/cli/templates/infra/config/stg/index.ts b/packages/cli/templates/infra/config/stg/index.ts new file mode 100644 index 0000000..36d92fc --- /dev/null +++ b/packages/cli/templates/infra/config/stg/index.ts @@ -0,0 +1,48 @@ +import { SystemLogLevel, ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda' +import { Config } from '../type' + +const config: Config = { + env: 'stg', + appName: '', + + domain: { + http: '', + appsync: '', + }, + + userPoolId: '', + + vpc: { + id: '', + subnetIds: [], + securityGroupIds: [], + }, + rds: { + accountSsmKey: '', + endpoint: '', + dbName: '', + }, + + logLevel: { + lambdaSystem: SystemLogLevel.DEBUG, + lambdaApplication: ApplicationLogLevel.TRACE, + level: 'info', + }, + + frontBaseUrl: '', + fromEmailAddress: '', + + // wafArn: '', + + // ecs: { + // maxInstances: 1, + // minInstances: 1, + // cpu: 512, + // memory: 1024, + // cpuThreshold: 70, + // scaleStep: 1, + // autoRollback: true, + // }, +} + +export default config diff --git a/packages/cli/templates/infra/config/type.ts b/packages/cli/templates/infra/config/type.ts new file mode 100644 index 0000000..18002e0 --- /dev/null +++ b/packages/cli/templates/infra/config/type.ts @@ -0,0 +1,53 @@ +import { ApplicationLogLevel, SystemLogLevel } from 'aws-cdk-lib/aws-lambda' + +export type Env = 'dev' | 'stg' | 'prod' + +export type Config = { + env: Env + appName: string + + // domain + domain: { + http: string + appsync: string + } + + // existing resources + userPoolId?: string + + vpc: { + id: string + subnetIds: string[] + securityGroupIds: string[] + } + + rds: { + accountSsmKey: string + endpoint: string + dbName: string + } + + systemAccountSsmKey?: string + + logLevel?: { + lambdaSystem?: SystemLogLevel + lambdaApplication?: ApplicationLogLevel + level?: 'verbose' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' + } + + frontBaseUrl: string + fromEmailAddress: string + + wafArn?: string + + ecs?: { + // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html + maxInstances: number + minInstances: number + cpu: number + memory: number + cpuThreshold?: number + scaleStep?: number + autoRollback?: boolean + } +} diff --git a/packages/cli/templates/infra/gitignore b/packages/cli/templates/infra/gitignore new file mode 100644 index 0000000..2d321b6 --- /dev/null +++ b/packages/cli/templates/infra/gitignore @@ -0,0 +1,12 @@ +*.js +!jest.config.js +!.cdkgraphrc.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +# +graph \ No newline at end of file diff --git a/packages/cli/templates/infra/jest.config.js b/packages/cli/templates/infra/jest.config.js new file mode 100644 index 0000000..e17f499 --- /dev/null +++ b/packages/cli/templates/infra/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + reporters: [ + 'default', + ['jest-junit', { outputDirectory: '../report', outputName: 'unit-infra.xml' }], + ], +} diff --git a/packages/cli/templates/infra/libs/build-app.ts b/packages/cli/templates/infra/libs/build-app.ts new file mode 100644 index 0000000..69a5e0e --- /dev/null +++ b/packages/cli/templates/infra/libs/build-app.ts @@ -0,0 +1,88 @@ +import { execSync } from 'child_process' +import { existsSync } from 'fs' +import * as path from 'path' +import { Env } from '../config' + +let isBuild = false + +export function buildApp(env: Env, isLocal = false) { + const cwd = path.resolve(__dirname, '../..') + console.log('build app started with cwd:', env, cwd) + + const layerPath = 'dist_layer' + const appPath = 'dist' + const layerFullPath = path.resolve(cwd, layerPath) + const appFullPath = path.resolve(cwd, appPath) + + if (isLocal || (existsSync(layerFullPath) && existsSync(appFullPath))) { + console.log('return from cached build folder') + return { + layerPath: layerFullPath, + appPath: appFullPath, + } + } + + const runCommand = function (cmd: string, cwdExec: string = cwd) { + console.log(cmd) + const ret = execSync(cmd, { cwd: cwdExec }) + console.log(ret.toString()) + } + + // clean up + console.log('============= clean up =============') + runCommand(`rm -rf ${layerPath}`) + runCommand(`rm -rf ${appPath}`) + runCommand(`rm -rf node_modules`) + + // install packages + console.log('============= install packages =============') + runCommand('npm ci') + + // build nestjs application + console.log('============= build nestjs application =============') + runCommand('npm run build:prod') + + // remove unnecessary packages + console.log('============= remove unnecessary packages =============') + runCommand('npm ci --omit=dev --omit=optional') + const prunePath = `${layerPath}/prune` + runCommand(`mkdir -p ${prunePath}`) + runCommand(`npm --prefix ./${prunePath} i node-prune modclean`) + runCommand( + `npm --prefix ./${prunePath} exec modclean -- -n default:safe,default:caution -r`, + ) + runCommand( + 'mv node_modules/.prisma/client/libquery_engine-linux-arm64-* prisma', + ) + runCommand('rm -rf node_modules/.prisma/client/libquery_engine-*') + runCommand( + 'mv prisma/libquery_engine-linux-arm64-* node_modules/.prisma/client', + ) + runCommand('rm -rf node_modules/prisma/libquery_engine-*') + runCommand('rm -rf node_modules/@prisma/engines/**') + + // copy to layer + console.log('============= copy to layer =============') + const nodejsLayerPath = `${layerPath}/nodejs` + runCommand(`mkdir -p ${nodejsLayerPath}`) + runCommand(`mv node_modules ${nodejsLayerPath}`) + + // min size layer + console.log('============= min size layer =============') + runCommand(`npm --prefix ../prune exec node-prune`, `${layerFullPath}/nodejs`) + runCommand(`rm -rf ${prunePath}`) + + console.log('============= build app finished =============') + + if (isLocal) { + console.log('============= install local packages =============') + runCommand('npm install') + } + + isBuild = true + + return { + layerPath: layerFullPath, + appPath: appFullPath, + } +} diff --git a/packages/cli/templates/infra/libs/infra-stack.ts b/packages/cli/templates/infra/libs/infra-stack.ts new file mode 100644 index 0000000..5199136 --- /dev/null +++ b/packages/cli/templates/infra/libs/infra-stack.ts @@ -0,0 +1,945 @@ +import * as cdk from 'aws-cdk-lib' +import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2' +import * as apigatewayv2_authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers' +import * as apigwv2_integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations' +import { Construct } from 'constructs' +import { randomBytes } from 'crypto' + +import { IgnoreMode } from 'aws-cdk-lib' +import { Repository } from 'aws-cdk-lib/aws-ecr' +import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets' +import { ContainerImage } from 'aws-cdk-lib/aws-ecs' +import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns' +import { DockerImageName, ECRDeployment } from 'cdk-ecr-deployment' +import * as path from 'path' +import { + ACM_APPSYNC_CERTIFICATE_ARN, + ACM_HTTP_CERTIFICATE_ARN, + HOSTED_ZONE_ID, + HOSTED_ZONE_NAME, +} from '../config' +import { Config } from '../config/type' +import { buildApp } from './build-app' + +export interface InfraStackProps extends cdk.StackProps { + config: Config +} + +export class InfraStack extends cdk.Stack { + public readonly userPoolId: cdk.CfnOutput + public readonly userPoolClientId: cdk.CfnOutput + public readonly graphqlApiUrl: cdk.CfnOutput + public readonly graphqlApiKey: cdk.CfnOutput + public readonly httpApiUrl: cdk.CfnOutput + public readonly stateMachineArn: cdk.CfnOutput + public readonly httpDistributionDomain: cdk.CfnOutput + + constructor(scope: Construct, id: string, props: InfraStackProps) { + super(scope, id, props) + + const name = props.config.appName + const env = props.config.env + const prefix = `${env}-${name}-` + const originVerifyToken = prefix + randomBytes(32).toString('hex') + + cdk.Tags.of(scope).add('name', props.config.appName) + cdk.Tags.of(scope).add('env', props.config.env) + + // Cognito + let userPool: cdk.aws_cognito.IUserPool + if (props.config.userPoolId) { + userPool = cdk.aws_cognito.UserPool.fromUserPoolId( + this, + 'main-user-pool', + props.config.userPoolId, + ) + } else { + // create new cognito + userPool = new cdk.aws_cognito.UserPool(this, prefix + 'user-pool', { + userPoolName: prefix + 'user-pool', + selfSignUpEnabled: false, + signInAliases: { + username: true, + preferredUsername: true, + }, + passwordPolicy: { + minLength: 6, + requireDigits: false, + requireLowercase: false, + requireSymbols: false, + requireUppercase: false, + }, + mfa: cdk.aws_cognito.Mfa.OFF, + accountRecovery: cdk.aws_cognito.AccountRecovery.NONE, + customAttributes: { + tenant_code: new cdk.aws_cognito.StringAttribute({ + mutable: true, + maxLen: 50, + }), + company_code: new cdk.aws_cognito.StringAttribute({ + mutable: true, + maxLen: 50, + }), + member_id: new cdk.aws_cognito.StringAttribute({ + mutable: true, + maxLen: 2024, + }), + roles: new cdk.aws_cognito.StringAttribute({ mutable: true }), + }, + email: cdk.aws_cognito.UserPoolEmail.withCognito(), + deletionProtection: true, + }) + } + this.userPoolId = new cdk.CfnOutput(this, 'UserPoolId', { + value: userPool.userPoolId, + }) + + // SNS + const mainSns = new cdk.aws_sns.Topic(this, 'main-sns', { + topicName: prefix + 'main-sns', + }) + + const alarmSns = new cdk.aws_sns.Topic(this, 'alarm-sns', { + topicName: prefix + 'alarm-sns', + }) + // SQS + const taskDlSqs = new cdk.aws_sqs.Queue(this, 'task-dead-letter-sqs', { + queueName: prefix + 'task-dead-letter-queue', + }) + const taskSqs = new cdk.aws_sqs.Queue(this, 'task-sqs', { + queueName: prefix + 'task-action-queue', + deadLetterQueue: { + queue: taskDlSqs, + maxReceiveCount: 5, + }, + }) + + alarmSns.addSubscription( + new cdk.aws_sns_subscriptions.SqsSubscription(taskDlSqs, { + rawMessageDelivery: true, + }), + ) + + mainSns.addSubscription( + new cdk.aws_sns_subscriptions.SqsSubscription(taskSqs, { + rawMessageDelivery: true, + filterPolicy: { + action: cdk.aws_sns.SubscriptionFilter.stringFilter({ + allowlist: ['task-execute'], + }), + }, + }), + ) + const notifySqs = new cdk.aws_sqs.Queue(this, 'notify-sqs', { + queueName: prefix + 'notification-queue', + }) + mainSns.addSubscription( + new cdk.aws_sns_subscriptions.SqsSubscription(notifySqs, { + rawMessageDelivery: true, + filterPolicy: { + action: cdk.aws_sns.SubscriptionFilter.stringFilter({ + allowlist: ['command-status', 'task-status'], + }), + }, + }), + ) + // host zone + const hostedZone = cdk.aws_route53.HostedZone.fromHostedZoneAttributes( + this, + 'HostedZone', + { + hostedZoneId: HOSTED_ZONE_ID, + zoneName: HOSTED_ZONE_NAME, + }, + ) + + // AppSync + const appSyncCertificate = + cdk.aws_certificatemanager.Certificate.fromCertificateArn( + this, + 'appsync-certificate', + ACM_APPSYNC_CERTIFICATE_ARN, + ) + + const appSyncApi = new cdk.aws_appsync.GraphqlApi(this, 'realtime', { + name: prefix + 'realtime', + definition: cdk.aws_appsync.Definition.fromFile('asset/schema.graphql'), // Define the schema + authorizationConfig: { + defaultAuthorization: { + authorizationType: cdk.aws_appsync.AuthorizationType.API_KEY, // Defining authorization type + apiKeyConfig: { + expires: cdk.Expiration.after(cdk.Duration.days(365)), // Set expiration for API Key + }, + }, + additionalAuthorizationModes: [ + { + authorizationType: cdk.aws_appsync.AuthorizationType.IAM, + }, + { + authorizationType: cdk.aws_appsync.AuthorizationType.USER_POOL, + userPoolConfig: { userPool }, + }, + ], + }, + xrayEnabled: true, // Enable X-Ray for monitoring + domainName: { + certificate: appSyncCertificate, + domainName: props.config.domain.appsync, + }, + }) + + const noneDS = appSyncApi.addNoneDataSource('NoneDataSource') + noneDS.createResolver('sendMessageResolver', { + typeName: 'Mutation', + fieldName: 'sendMessage', + requestMappingTemplate: cdk.aws_appsync.MappingTemplate.fromString( + '{"version": "2018-05-29","payload": $util.toJson($context.arguments.message)}', + ), + responseMappingTemplate: cdk.aws_appsync.MappingTemplate.fromString( + '$util.toJson($context.result)', + ), + }) + + // route to AppSync + new cdk.aws_route53.CnameRecord(this, `AppSyncCnameRecord`, { + zone: hostedZone, + recordName: props.config.domain.appsync, + domainName: appSyncApi.appSyncDomainName, + }) + + this.graphqlApiUrl = new cdk.CfnOutput(this, 'GraphQLAPIURL', { + value: appSyncApi.graphqlUrl, + }) + this.graphqlApiKey = new cdk.CfnOutput(this, 'GraphQLAPIKey', { + value: appSyncApi.apiKey || '', + }) + // S3 + const ddbBucket = new cdk.aws_s3.Bucket(this, 'ddb-attributes', { + bucketName: prefix + 'ddb-attributes', // Globally unique bucket name + versioned: false, + publicReadAccess: false, // Block public read access + blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, // Block all public access + removalPolicy: cdk.RemovalPolicy.DESTROY, // Define removal policy (use with caution in production) + cors: [ + { + allowedHeaders: ['*'], + allowedMethods: [ + cdk.aws_s3.HttpMethods.GET, + cdk.aws_s3.HttpMethods.PUT, + cdk.aws_s3.HttpMethods.POST, + ], + allowedOrigins: ['*'], + maxAge: 3000, + }, + ], + }) + + const publicBucket = new cdk.aws_s3.Bucket(this, 'public-bucket', { + bucketName: prefix + 'public', // Globally unique bucket name + versioned: false, + publicReadAccess: false, // Block public read access + blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, // Block all public access + removalPolicy: cdk.RemovalPolicy.DESTROY, // Define removal policy (use with caution in production) + cors: [ + { + allowedHeaders: ['*'], + allowedMethods: [ + cdk.aws_s3.HttpMethods.GET, + cdk.aws_s3.HttpMethods.PUT, + cdk.aws_s3.HttpMethods.POST, + ], + allowedOrigins: ['*'], + maxAge: 3000, + }, + ], + }) + // cloudfront + const publicBucketOAI = new cdk.aws_cloudfront.OriginAccessIdentity( + this, + 'public-bucket-OAI', + ) + publicBucket.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + actions: ['s3:GetObject'], + effect: cdk.aws_iam.Effect.ALLOW, + resources: [publicBucket.arnForObjects('*')], + principals: [ + new cdk.aws_iam.CanonicalUserPrincipal( + publicBucketOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId, + ), + ], + }), + ) + const publicBucketDistribution = new cdk.aws_cloudfront.Distribution( + this, + 'public-bucket-distribution', + { + defaultBehavior: { + allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD, + cachedMethods: cdk.aws_cloudfront.CachedMethods.CACHE_GET_HEAD, + cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_OPTIMIZED, + viewerProtocolPolicy: + cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + origin: new cdk.aws_cloudfront_origins.S3Origin(publicBucket, { + originAccessIdentity: publicBucketOAI, + }), + }, + priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_200, + geoRestriction: cdk.aws_cloudfront.GeoRestriction.allowlist('JP', 'VN'), + }, + ) + + // VPC + const vpc = cdk.aws_ec2.Vpc.fromLookup(this, 'main-vpc', { + vpcId: props.config.vpc.id, + }) + + const subnets = cdk.aws_ec2.SubnetFilter.byIds(props.config.vpc.subnetIds) + const securityGroups = props.config.vpc.securityGroupIds.map((id, idx) => + cdk.aws_ec2.SecurityGroup.fromSecurityGroupId( + this, + 'main-security-group-' + idx, + id, + ), + ) + // Lambda Layer + const { layerPath, appPath } = buildApp(env) + console.log('dist path:', layerPath, appPath) + const lambdaLayer = new cdk.aws_lambda.LayerVersion(this, 'main-layer', { + layerVersionName: prefix + 'main-layer', + code: cdk.aws_lambda.AssetCode.fromAsset(layerPath), + compatibleRuntimes: [cdk.aws_lambda.Runtime.NODEJS_18_X], + compatibleArchitectures: [cdk.aws_lambda.Architecture.ARM_64], + }) + + const commandSfnArn = cdk.Arn.format({ + partition: 'aws', + region: this.region, + account: this.account, + service: 'states', + resource: 'stateMachine', + resourceName: prefix + 'command-handler', + arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME, + }) + + // Lambda api ( arm64 ) + const execEnv = { + NODE_OPTIONS: '--enable-source-maps', + TZ: 'Asia/Tokyo', + NODE_ENV: env, + APP_NAME: name, + LOG_LEVEL: props.config.logLevel?.level || 'info', + EVENT_SOURCE_DISABLED: 'false', + ATTRIBUTE_LIMIT_SIZE: '389120', + S3_BUCKET_NAME: ddbBucket.bucketName, + SFN_COMMAND_ARN: commandSfnArn, + SNS_TOPIC_ARN: mainSns.topicArn, + COGNITO_USER_POOL_ID: userPool.userPoolId, + APPSYNC_ENDPOINT: appSyncApi.graphqlUrl, + SES_FROM_EMAIL: props.config.fromEmailAddress, + DATABASE_URL: `postgresql://${props.config.rds.accountSsmKey}@${props.config.rds.endpoint}/${props.config.rds.dbName}?schema=public`, + S3_PUBLIC_BUCKET_NAME: publicBucket.bucketName, + FRONT_BASE_URL: props.config.frontBaseUrl, + } + const lambdaApi = new cdk.aws_lambda.Function(this, 'lambda-api', { + vpc, + vpcSubnets: { + subnetFilters: [subnets], + }, + securityGroups, + architecture: cdk.aws_lambda.Architecture.ARM_64, + functionName: prefix + 'lambda-api', + layers: [lambdaLayer], + code: cdk.aws_lambda.Code.fromAsset(appPath), + handler: 'main.handler', + runtime: cdk.aws_lambda.Runtime.NODEJS_LATEST, + timeout: cdk.Duration.seconds(30), + memorySize: 512, + tracing: cdk.aws_lambda.Tracing.ACTIVE, + loggingFormat: cdk.aws_lambda.LoggingFormat.JSON, + applicationLogLevelV2: props.config.logLevel?.lambdaApplication, + systemLogLevelV2: props.config.logLevel?.lambdaSystem, + environment: execEnv, + }) + + // API GW + const httpApi = new apigwv2.HttpApi(this, 'main-api', { + description: 'HTTP API for Lambda integration', + apiName: prefix + 'api', + corsPreflight: { + allowOrigins: ['*'], + allowCredentials: false, + allowHeaders: ['*'], + allowMethods: [apigwv2.CorsHttpMethod.ANY], + maxAge: cdk.Duration.hours(1), + }, + }) + const lambdaIntegration = new apigwv2_integrations.HttpLambdaIntegration( + 'main-api-lambda', + lambdaApi, + ) + // event routes + httpApi.addRoutes({ + path: '/event/{proxy+}', + integration: lambdaIntegration, + authorizer: new apigatewayv2_authorizers.HttpIamAuthorizer(), + }) + + // api protected routes + const userPoolClient = new cdk.aws_cognito.UserPoolClient( + this, + 'apigw-client', + { + userPool, + authFlows: { + userPassword: true, + userSrp: true, + }, + }, + ) + + this.userPoolClientId = new cdk.CfnOutput(this, 'UserPoolClientId', { + value: userPoolClient.userPoolClientId, + }) + + const authorizer = new apigatewayv2_authorizers.HttpUserPoolAuthorizer( + 'CognitoAuthorizer', + userPool, + { + userPoolClients: [userPoolClient], + }, + ) + + let apiIntegration: apigwv2.HttpRouteIntegration + let taskRole: cdk.aws_iam.Role | undefined + if (!props.config.ecs) { + apiIntegration = lambdaIntegration + } else { + // ecs api + const resp = new Repository(this, 'main-ecr-repo', { + repositoryName: `${prefix}api`, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }) + + const image = new DockerImageAsset(this, 'main-image', { + directory: path.resolve(__dirname, '../..'), + platform: Platform.LINUX_AMD64, + ignoreMode: IgnoreMode.DOCKER, + }) + + const imageTag = process.env.CODEBUILD_RESOLVED_SOURCE_VERSION + ? process.env.CODEBUILD_RESOLVED_SOURCE_VERSION.substring(0, 4) + : 'latest' + + new ECRDeployment(this, `${prefix}deploy`, { + src: new DockerImageName(image.imageUri), + dest: new DockerImageName(`${resp.repositoryUri}:${imageTag}`), + }) + + taskRole = new cdk.aws_iam.Role(this, 'ecs-role', { + assumedBy: new cdk.aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }) + + const ecsService = new ApplicationLoadBalancedFargateService( + this, + 'main-service', + { + vpc, + taskSubnets: { + subnetFilters: [subnets], + }, + securityGroups, + circuitBreaker: { + rollback: props.config.ecs.autoRollback, + }, + publicLoadBalancer: false, + memoryLimitMiB: props.config.ecs.memory, + cpu: props.config.ecs.cpu, + desiredCount: props.config.ecs.minInstances, + taskImageOptions: { + image: ContainerImage.fromDockerImageAsset(image), + environment: { + ...execEnv, + APP_PORT: '80', + EVENT_SOURCE_DISABLED: 'true', + PRISMA_EXPLICIT_CONNECT: 'false', + }, + secrets: { + DATABASE_USER_PASS: cdk.aws_ecs.Secret.fromSsmParameter( + cdk.aws_ssm.StringParameter.fromSecureStringParameterAttributes( + this, + 'dbUserPass', + { + parameterName: props.config.rds.accountSsmKey, + }, + ), + ), + }, + taskRole, + }, + }, + ) + + if (props.config.ecs.cpuThreshold) { + const scalableTarget = ecsService.service.autoScaleTaskCount({ + minCapacity: props.config.ecs.minInstances, + maxCapacity: props.config.ecs.maxInstances, + }) + + scalableTarget.scaleOnCpuUtilization('CpuScaling', { + targetUtilizationPercent: props.config.ecs.cpuThreshold, + }) + } + + const vpcLink = new apigwv2.VpcLink(this, 'ecs-vpc-link', { + vpc, + }) + const vpcLinkIntegration = new apigwv2_integrations.HttpAlbIntegration( + 'ecs-vpc-link-integration', + ecsService.loadBalancer.listeners[0], + { + vpcLink, + parameterMapping: new apigwv2.ParameterMapping() + .appendHeader( + 'x-source-ip', + apigwv2.MappingValue.contextVariable('identity.sourceIp'), + ) + .appendHeader( + 'x-request-id', + apigwv2.MappingValue.contextVariable('extendedRequestId'), + ), + }, + ) + apiIntegration = vpcLinkIntegration + } + // health check api (public) + httpApi.addRoutes({ + path: '/', + methods: [apigwv2.HttpMethod.GET], + integration: apiIntegration, + }) + // protected api + httpApi.addRoutes({ + path: '/{proxy+}', + methods: [ + apigwv2.HttpMethod.HEAD, + apigwv2.HttpMethod.GET, + apigwv2.HttpMethod.POST, + apigwv2.HttpMethod.DELETE, + apigwv2.HttpMethod.PUT, + apigwv2.HttpMethod.PATCH, + ], + integration: apiIntegration, + authorizer, + }) + // Output the URL of the HTTP API + this.httpApiUrl = new cdk.CfnOutput(this, 'HttpApiUrl', { + value: httpApi.url!, + }) + + // cloudfront to HTTP API + const httpDistributionCertificate = + cdk.aws_certificatemanager.Certificate.fromCertificateArn( + this, + 'http-distribution-certificate', + ACM_HTTP_CERTIFICATE_ARN, + ) + const httpDistribution = new cdk.aws_cloudfront.Distribution( + this, + 'http-distribution', + { + defaultBehavior: { + origin: new cdk.aws_cloudfront_origins.HttpOrigin( + `${httpApi.apiId}.execute-api.${this.region}.amazonaws.com`, + { + customHeaders: { + 'X-Origin-Verify': originVerifyToken, + }, + }, + ), + originRequestPolicy: + cdk.aws_cloudfront.OriginRequestPolicy + .ALL_VIEWER_EXCEPT_HOST_HEADER, + responseHeadersPolicy: + cdk.aws_cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS, + allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_ALL, + cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_DISABLED, + viewerProtocolPolicy: + cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + }, + priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_200, + geoRestriction: cdk.aws_cloudfront.GeoRestriction.allowlist('JP', 'VN'), + domainNames: [props.config.domain.http], + certificate: httpDistributionCertificate, + webAclId: props.config.wafArn, + enableIpv6: false, + }, + ) + + new cdk.aws_route53.CnameRecord(this, 'http-distribution-a-record', { + zone: hostedZone, + recordName: props.config.domain.http, + domainName: httpDistribution.distributionDomainName, + }) + + this.httpDistributionDomain = new cdk.CfnOutput( + this, + 'http-distribution-domain', + { + value: httpDistribution.distributionDomainName, + }, + ) + + // api gateway logging + // Setup the access log for APIGWv2 + const httpApiAccessLogs = new cdk.aws_logs.LogGroup( + this, + 'http-api-AccessLogs', + ) + const httpApiDefaultStage = httpApi.defaultStage?.node + .defaultChild as cdk.aws_apigatewayv2.CfnStage + httpApiDefaultStage.accessLogSettings = { + destinationArn: httpApiAccessLogs.logGroupArn, + format: JSON.stringify({ + requestId: '$context.requestId', + ip: '$context.identity.sourceIp', + userAgent: '$context.identity.userAgent', + sourceIp: '$context.identity.sourceIp', + requestTime: '$context.requestTime', + requestTimeEpoch: '$context.requestTimeEpoch', + httpMethod: '$context.httpMethod', + routeKey: '$context.routeKey', + path: '$context.path', + status: '$context.status', + protocol: '$context.protocol', + responseLength: '$context.responseLength', + domainName: '$context.domainName', + responseLatency: '$context.responseLatency', + integrationLatency: '$context.integrationLatency', + username: '$context.authorizer.claims.sub', + }), + } + httpApiDefaultStage.defaultRouteSettings = { + detailedMetricsEnabled: true, + } + + // StepFunction + // Define the lambda invoke task with common configurations + const lambdaInvoke = ( + stateName: string, + nextState: cdk.aws_stepfunctions.IChainable | null, + integrationPattern: cdk.aws_stepfunctions.IntegrationPattern, + ) => { + const payloadObject: { + [key: string]: any + } = { + 'input.$': '$', + 'context.$': '$$', + } + if ( + integrationPattern === + cdk.aws_stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN + ) { + payloadObject['taskToken'] = cdk.aws_stepfunctions.JsonPath.taskToken // '$$.Task.Token' + } + const lambdaTask = new cdk.aws_stepfunctions_tasks.LambdaInvoke( + this, + stateName, + { + lambdaFunction: lambdaApi, + payload: cdk.aws_stepfunctions.TaskInput.fromObject(payloadObject), + retryOnServiceExceptions: true, + stateName, + outputPath: '$.Payload[0][0]', + integrationPattern, + }, + ) + if (nextState) { + return lambdaTask.next(nextState) + } + return lambdaTask + } + + // Define states + const fail = new cdk.aws_stepfunctions.Fail(this, 'fail', { + stateName: 'fail', + causePath: '$.cause', + errorPath: '$.error', + }) + const success = new cdk.aws_stepfunctions.Succeed(this, 'success', { + stateName: 'success', + }) + const finish = lambdaInvoke( + 'finish', + success, + cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE, + ) + const syncData = lambdaInvoke( + 'sync_data', + null, + cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE, + ) + // Define Map state + const syncDataAll = new cdk.aws_stepfunctions.Map(this, 'sync_data_all', { + stateName: 'sync_data_all', + maxConcurrency: 0, + itemsPath: cdk.aws_stepfunctions.JsonPath.stringAt('$'), + }) + .itemProcessor(syncData) + .next(finish) + const transformData = lambdaInvoke( + 'transform_data', + syncDataAll, + cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE, + ) + const historyCopy = lambdaInvoke( + 'history_copy', + transformData, + cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE, + ) + const waitPrevCommand = lambdaInvoke( + 'wait_prev_command', + historyCopy, + cdk.aws_stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ) + + // Define Choice state + const checkVersionResult = new cdk.aws_stepfunctions.Choice( + this, + 'check_version_result', + { + stateName: 'check_version_result', + }, + ) + .when( + cdk.aws_stepfunctions.Condition.numberEquals('$.result', 0), + historyCopy, + ) + .when( + cdk.aws_stepfunctions.Condition.numberEquals('$.result', 1), + waitPrevCommand, + ) + .when(cdk.aws_stepfunctions.Condition.numberEquals('$.result', -1), fail) + .otherwise(waitPrevCommand) + + const sfnDefinition = lambdaInvoke( + 'check_version', + checkVersionResult, + cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE, + ) + + const sfnLogGroup = new cdk.aws_logs.LogGroup( + this, + 'command-handler-sfn-log', + { + logGroupName: `/aws/vendedlogs/states/${prefix}-command-handler-state-machine-Logs`, // Specify a log group name + removalPolicy: cdk.RemovalPolicy.DESTROY, // Policy for log group removal + retention: cdk.aws_logs.RetentionDays.SIX_MONTHS, + }, + ) + + // Define the state machine + const stateMachine = new cdk.aws_stepfunctions.StateMachine( + this, + 'command-handler-state-machine', + { + stateMachineName: prefix + 'command-handler', + comment: 'A state machine that run the command stream handler', + definitionBody: + cdk.aws_stepfunctions.DefinitionBody.fromChainable(sfnDefinition), + tracingEnabled: true, + logs: { + destination: sfnLogGroup, + level: cdk.aws_stepfunctions.LogLevel.ALL, // Log level (ALL, ERROR, or FATAL) + }, + }, + ) + + // Output the State Machine's ARN + this.stateMachineArn = new cdk.CfnOutput(this, 'StateMachineArn', { + value: stateMachine.stateMachineArn, + }) + + // add event sources to lambda event + lambdaApi.addEventSource( + new cdk.aws_lambda_event_sources.SqsEventSource(taskSqs, { + batchSize: 1, + }), + ) + lambdaApi.addEventSource( + new cdk.aws_lambda_event_sources.SqsEventSource(notifySqs, { + batchSize: 1, + }), + ) + // dynamodb event source + const tableNames = ['tasks', 'master-command'] + for (const tableName of tableNames) { + const tableDesc = new cdk.custom_resources.AwsCustomResource( + this, + tableName + '-decs', + { + onCreate: { + service: 'DynamoDB', + action: 'describeTable', + parameters: { + TableName: prefix + tableName, + }, + physicalResourceId: + cdk.custom_resources.PhysicalResourceId.fromResponse( + 'Table.TableArn', + ), + }, + policy: cdk.custom_resources.AwsCustomResourcePolicy.fromSdkCalls({ + resources: + cdk.custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE, + }), + }, + ) + const tableCdk = cdk.aws_dynamodb.Table.fromTableAttributes( + this, + tableName + '-table', + { + tableArn: tableDesc.getResponseField('Table.TableArn'), + tableStreamArn: tableDesc.getResponseField('Table.LatestStreamArn'), + }, + ) + lambdaApi.addEventSource( + new cdk.aws_lambda_event_sources.DynamoEventSource(tableCdk, { + startingPosition: cdk.aws_lambda.StartingPosition.TRIM_HORIZON, + batchSize: 1, + filters: [ + cdk.aws_lambda.FilterCriteria.filter({ + eventName: cdk.aws_lambda.FilterRule.isEqual('INSERT'), + }), + ], + }), + ) + } + + // add lambda role + userPool.grant( + lambdaApi, + 'cognito-idp:AdminGetUser', + 'cognito-idp:AdminAddUserToGroup', + 'cognito-idp:AdminCreateUser', + 'cognito-idp:AdminDeleteUser', + 'cognito-idp:AdminDisableUser', + 'cognito-idp:AdminEnableUser', + 'cognito-idp:AdminSetUserPassword', + 'cognito-idp:AdminResetUserPassword', + 'cognito-idp:AdminUpdateUserAttributes', + ) + ddbBucket.grantReadWrite(lambdaApi) + publicBucket.grantReadWrite(lambdaApi) + mainSns.grantPublish(lambdaApi) + taskSqs.grantSendMessages(lambdaApi) + notifySqs.grantSendMessages(lambdaApi) + appSyncApi.grantMutation(lambdaApi) + + // Define an IAM policy for full DynamoDB access + const dynamoDbTablePrefixArn = cdk.Arn.format({ + partition: 'aws', + region: this.region, + account: this.account, + service: 'dynamodb', + resource: 'table', + resourceName: prefix + '*', + }) + const dynamodbPolicy = new cdk.aws_iam.PolicyStatement({ + actions: [ + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + 'dynamodb:GetItem', + 'dynamodb:Query', + ], + resources: [dynamoDbTablePrefixArn], // Access to all resources + }) + + // Attach the policy to the Lambda function's execution role + lambdaApi.role?.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'lambda-api-ddb-policy', { + statements: [dynamodbPolicy], + }), + ) + + const sfnPolicy = new cdk.aws_iam.PolicyStatement({ + actions: [ + 'states:StartExecution', + 'states:GetExecutionHistory', + 'states:DescribeExecution', + ], + resources: [commandSfnArn], + }) + + // Attach the policy to the Lambda function's execution role + lambdaApi.role?.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'lambda-event-sfn-policy', { + statements: [sfnPolicy], + }), + ) + + const sesPolicy = new cdk.aws_iam.PolicyStatement({ + actions: ['ses:SendEmail'], + resources: ['*'], + }) + + // Attach the policy to the Lambda function's execution role + lambdaApi.role?.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'lambda-ses-policy', { + statements: [sesPolicy], + }), + ) + + const ssmPolicy = new cdk.aws_iam.PolicyStatement({ + actions: ['ssm:GetParameter', 'kms:Decrypt'], + resources: ['*'], + }) + + // allow lambdaApi role to access ssm + lambdaApi.role?.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'lambda-api-ssm-policy', { + statements: [ssmPolicy], + }), + ) + + if (!!taskRole) { + ddbBucket.grantReadWrite(taskRole) + publicBucket.grantReadWrite(taskRole) + mainSns.grantPublish(taskRole) + taskSqs.grantSendMessages(taskRole) + notifySqs.grantSendMessages(taskRole) + appSyncApi.grantMutation(taskRole) + taskRole.addToPrincipalPolicy( + new cdk.aws_iam.PolicyStatement({ + actions: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + ], + resources: ['*'], + }), + ) + taskRole.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'ecs-api-ddb-policy', { + statements: [dynamodbPolicy], + }), + ) + taskRole.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'ecs-event-sfn-policy', { + statements: [sfnPolicy], + }), + ) + taskRole.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'ecs-ses-policy', { + statements: [sesPolicy], + }), + ) + taskRole.attachInlinePolicy( + new cdk.aws_iam.Policy(this, 'ecs-api-ssm-policy', { + statements: [ssmPolicy], + }), + ) + } + } +} diff --git a/packages/cli/templates/infra/libs/pipeline-infra-stage.ts b/packages/cli/templates/infra/libs/pipeline-infra-stage.ts new file mode 100644 index 0000000..5eec282 --- /dev/null +++ b/packages/cli/templates/infra/libs/pipeline-infra-stage.ts @@ -0,0 +1,34 @@ +import { CfnOutput, Stage, StageProps } from 'aws-cdk-lib' +import { Construct } from 'constructs' +import { InfraStack } from './infra-stack' +import { getConfig } from '../config' +import { Env } from '../config/type' + +export interface PipelineInfraStageProps extends StageProps { + appEnv: Env +} + +export class PipelineInfraStage extends Stage { + public readonly userPoolId: CfnOutput + public readonly userPoolClientId: CfnOutput + public readonly graphqlApiUrl: CfnOutput + public readonly graphqlApiKey: CfnOutput + public readonly httpApiUrl: CfnOutput + public readonly httpDistributionDomain: CfnOutput + + constructor(scope: Construct, id: string, props: PipelineInfraStageProps) { + super(scope, id, props) + + const config = getConfig(props.appEnv) + const infraStack = new InfraStack(this, props.appEnv + 'InfraStack', { + config, + }) + + this.userPoolId = infraStack.userPoolId + this.userPoolClientId = infraStack.userPoolClientId + this.graphqlApiUrl = infraStack.graphqlApiUrl + this.graphqlApiKey = infraStack.graphqlApiKey + this.httpApiUrl = infraStack.httpApiUrl + this.httpDistributionDomain = infraStack.httpDistributionDomain + } +} diff --git a/packages/cli/templates/infra/libs/pipeline-stack.ts b/packages/cli/templates/infra/libs/pipeline-stack.ts new file mode 100644 index 0000000..9354ce4 --- /dev/null +++ b/packages/cli/templates/infra/libs/pipeline-stack.ts @@ -0,0 +1,120 @@ +import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib' +import { BuildSpec, ReportGroup } from 'aws-cdk-lib/aws-codebuild' +import { + CodeBuildStep, + CodePipeline, + CodePipelineSource, + ShellStep, +} from 'aws-cdk-lib/pipelines' +import { Construct } from 'constructs' +import { + GIT_CONNECTION_ARN, + GIT_REPO, + PIPELINE_NAME, + getConfig, +} from '../config' +import { Env } from '../config/type' +import { PipelineInfraStage } from './pipeline-infra-stage' + +export interface PipelineStackProps extends StackProps { + envName: Env +} + +const mappingBranchName = { + dev: 'develop', + stg: 'staging', +} + +export class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: PipelineStackProps) { + super(scope, id, props) + + const env = props?.envName || 'dev' + + const idName = env.charAt(0).toUpperCase() + env.slice(1) + const branchName = + mappingBranchName[env as keyof typeof mappingBranchName] || 'main' + + const config = getConfig(env) + const name = config.appName + + const prefix = `${env}-${name}-` + + const unitTestReports = new ReportGroup(this, `${prefix}UnitTestReports`, { + reportGroupName: `${prefix}UnitTestReports`, + removalPolicy: RemovalPolicy.DESTROY, + }) + + const testStep = new CodeBuildStep(`${prefix}Test`, { + projectName: `${prefix}Test`, + installCommands: [ + 'npm i -g pnpm', + 'npm ci', + 'pnpm --dir ./infra install --frozen-lockfile', + ], + commands: ['npm run test', 'npm --prefix ./infra run test'], + primaryOutputDirectory: 'report', + partialBuildSpec: BuildSpec.fromObject({ + reports: { + [unitTestReports.reportGroupArn]: { + files: ['*.xml'], + 'base-directory': 'report', + 'discard-paths': true, + }, + }, + }), + }) + + const pipeline = new CodePipeline(this, idName + 'PipelineV3', { + selfMutation: true, + synth: new ShellStep('Synth', { + input: CodePipelineSource.connection(GIT_REPO, branchName, { + connectionArn: GIT_CONNECTION_ARN, + }), + additionalInputs: { + testOut: testStep, + }, + commands: [ + 'npm i -g pnpm', + 'cd infra', + 'pnpm install --frozen-lockfile', + 'npm run build', + 'npx cdk synth ' + id + ' -e', + ], + primaryOutputDirectory: 'infra/cdk.out', + }), + }) + + const infraPipelineStage = new PipelineInfraStage( + this, + idName + PIPELINE_NAME + 'InfraStage', + { + appEnv: env, + env: { account: this.account, region: this.region }, + }, + ) + + const infraStage = pipeline.addStage(infraPipelineStage) + infraStage.addPost( + new ShellStep('validate', { + envFromCfnOutputs: { + OUTPUT_HTTP_API_URL: infraPipelineStage.httpApiUrl, + OUTPUT_GRAPHQL_API_URL: infraPipelineStage.graphqlApiUrl, + OUTPUT_GRAPHQL_API_KEY: infraPipelineStage.graphqlApiKey, + OUTPUT_USER_POOL_ID: infraPipelineStage.userPoolId, + OUTPUT_HTTP_DISTRIBUTION_DOMAIN: + infraPipelineStage.httpDistributionDomain, + }, + commands: [ + 'echo $OUTPUT_HTTP_API_URL', + 'echo $OUTPUT_GRAPHQL_API_URL', + 'echo $OUTPUT_GRAPHQL_API_KEY', + 'echo $OUTPUT_USER_POOL_ID', + 'echo $OUTPUT_HTTP_DISTRIBUTION_DOMAIN', + ], + }), + ) + pipeline.buildPipeline() + unitTestReports.grantWrite(testStep.grantPrincipal) + } +} diff --git a/packages/cli/templates/infra/package.json b/packages/cli/templates/infra/package.json new file mode 100644 index 0000000..d046376 --- /dev/null +++ b/packages/cli/templates/infra/package.json @@ -0,0 +1,35 @@ +{ + "name": "infra", + "version": "0.1.0", + "bin": { + "infra": "bin/infra.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.10", + "@types/node": "^20.9.4", + "aws-cdk": "^2.147.0", + "jest": "^29.7.0", + "jest-junit": "^16.0.0", + "prettier": "^3.1.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.3.2" + }, + "dependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "^2.114.1-alpha.0", + "@aws-cdk/aws-apigatewayv2-authorizers-alpha": "^2.114.1-alpha.0", + "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.114.1-alpha.0", + "@aws/pdk": "^0.25.7", + "aws-cdk-lib": "^2.147.0", + "cdk-ecr-deployment": "^3.0.71", + "constructs": "^10.3.0", + "dotenv": "^16.3.1", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/cli/templates/infra/test/.gitkeep b/packages/cli/templates/infra/test/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/templates/infra/test/__snapshots__/infra.test.ts.snap b/packages/cli/templates/infra/test/__snapshots__/infra.test.ts.snap new file mode 100644 index 0000000..b64cc8d --- /dev/null +++ b/packages/cli/templates/infra/test/__snapshots__/infra.test.ts.snap @@ -0,0 +1,2392 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test for InfraStack 1`] = ` +{ + "Outputs": { + "GraphQLAPIKey": { + "Value": { + "Fn::GetAtt": [ + "realtimeDefaultApiKeyBD0EC5CB", + "ApiKey", + ], + }, + }, + "GraphQLAPIURL": { + "Value": { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "GraphQLUrl", + ], + }, + }, + "HttpApiUrl": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "mainapiC7DC6378", + }, + ".execute-api.ap-northeast-1.", + { + "Ref": "AWS::URLSuffix", + }, + "/", + ], + ], + }, + }, + "StateMachineArn": { + "Value": { + "Ref": "commandhandlerstatemachine937D91FB", + }, + }, + "UserPoolClientId": { + "Value": { + "Ref": "apigwclientE7D084A5", + }, + }, + "UserPoolId": { + "Value": "ap-northeast-1_xlQVMPxtx", + }, + "httpdistributiondomain": { + "Value": { + "Fn::GetAtt": [ + "httpdistribution87A15827", + "DomainName", + ], + }, + }, + }, + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + ], + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-101010101010-ap-northeast-1", + "S3Key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.zip", + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + "Timeout": 120, + }, + "Type": "AWS::Lambda::Function", + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "AppSyncCnameRecord408D9525": { + "Properties": { + "HostedZoneId": "Z083753814XLAICIY5OHU", + "Name": "dev-appsync-cdk.mbc-cqrs-serverless.mbc-net.com.", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "realtimeDomainName01674B94", + "AppSyncDomainName", + ], + }, + ], + "TTL": "1800", + "Type": "CNAME", + }, + "Type": "AWS::Route53::RecordSet", + }, + "alarmsnsFB7BBC3B": { + "Properties": { + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + "TopicName": "dev-cdk-test-deploy-alarm-sns", + }, + "Type": "AWS::SNS::Topic", + }, + "apigwclientE7D084A5": { + "Properties": { + "AllowedOAuthFlows": [ + "implicit", + "code", + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin", + ], + "CallbackURLs": [ + "https://example.com", + ], + "ExplicitAuthFlows": [ + "ALLOW_USER_PASSWORD_AUTH", + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH", + ], + "SupportedIdentityProviders": [ + "COGNITO", + ], + "UserPoolId": "ap-northeast-1_xlQVMPxtx", + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "commandhandlersfnlog9072E963": { + "DeletionPolicy": "Delete", + "Properties": { + "LogGroupName": "/aws/vendedlogs/states/dev-cdk-test-deploy--command-handler-state-machine-Logs", + "RetentionInDays": 180, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Delete", + }, + "commandhandlerstatemachine937D91FB": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "commandhandlerstatemachineRoleDefaultPolicy347D4054", + "commandhandlerstatemachineRoleC4EA8D70", + ], + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{"StartAt":"check_version","States":{"check_version":{"Next":"check_version_result","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload[0][0]","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "","Payload":{"input.$":"$","context.$":"$$"}}},"check_version_result":{"Type":"Choice","Choices":[{"Variable":"$.result","NumericEquals":0,"Next":"history_copy"},{"Variable":"$.result","NumericEquals":1,"Next":"wait_prev_command"},{"Variable":"$.result","NumericEquals":-1,"Next":"fail"}],"Default":"wait_prev_command"},"wait_prev_command":{"Next":"history_copy","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload[0][0]","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke.waitForTaskToken","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "","Payload":{"input.$":"$","context.$":"$$","taskToken.$":"$$.Task.Token"}}},"history_copy":{"Next":"transform_data","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload[0][0]","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "","Payload":{"input.$":"$","context.$":"$$"}}},"transform_data":{"Next":"sync_data_all","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload[0][0]","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "","Payload":{"input.$":"$","context.$":"$$"}}},"sync_data_all":{"Type":"Map","Next":"finish","ItemsPath":"$","ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},"StartAt":"sync_data","States":{"sync_data":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload[0][0]","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "","Payload":{"input.$":"$","context.$":"$$"}}}}}},"finish":{"Next":"success","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload[0][0]","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "","Payload":{"input.$":"$","context.$":"$$"}}},"success":{"Type":"Succeed"},"fail":{"Type":"Fail","ErrorPath":"$.error","CausePath":"$.cause"}},"Comment":"A state machine that run the command stream handler"}", + ], + ], + }, + "LoggingConfiguration": { + "Destinations": [ + { + "CloudWatchLogsLogGroup": { + "LogGroupArn": { + "Fn::GetAtt": [ + "commandhandlersfnlog9072E963", + "Arn", + ], + }, + }, + }, + ], + "Level": "ALL", + }, + "RoleArn": { + "Fn::GetAtt": [ + "commandhandlerstatemachineRoleC4EA8D70", + "Arn", + ], + }, + "StateMachineName": "dev-cdk-test-deploy-command-handler", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + "TracingConfiguration": { + "Enabled": true, + }, + }, + "Type": "AWS::StepFunctions::StateMachine", + "UpdateReplacePolicy": "Delete", + }, + "commandhandlerstatemachineRoleC4EA8D70": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "commandhandlerstatemachineRoleDefaultPolicy347D4054": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + { + "Action": [ + "logs:CreateLogDelivery", + "logs:GetLogDelivery", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:ListLogDeliveries", + "logs:PutResourcePolicy", + "logs:DescribeResourcePolicies", + "logs:DescribeLogGroups", + ], + "Effect": "Allow", + "Resource": "*", + }, + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + "xray:GetSamplingRules", + "xray:GetSamplingTargets", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "commandhandlerstatemachineRoleDefaultPolicy347D4054", + "Roles": [ + { + "Ref": "commandhandlerstatemachineRoleC4EA8D70", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "ddbattributes95C63151": { + "DeletionPolicy": "Delete", + "Properties": { + "BucketName": "dev-cdk-test-deploy-ddb-attributes", + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedHeaders": [ + "*", + ], + "AllowedMethods": [ + "GET", + "PUT", + "POST", + ], + "AllowedOrigins": [ + "*", + ], + "MaxAge": 3000, + }, + ], + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + }, + "httpapiAccessLogs0179ABB1": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "httpdistribution87A15827": { + "Properties": { + "DistributionConfig": { + "Aliases": [ + "dev-api-cdk.mbc-cqrs-serverless.mbc-net.com", + ], + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD", + "OPTIONS", + "PUT", + "PATCH", + "POST", + "DELETE", + ], + "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", + "Compress": true, + "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac", + "ResponseHeadersPolicyId": "60669652-455b-4ae9-85a4-c4c02393f86c", + "TargetOriginId": "TestInfraStackhttpdistributionOrigin13C4D9587", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": false, + "Origins": [ + { + "CustomOriginConfig": { + "OriginProtocolPolicy": "https-only", + "OriginSSLProtocols": [ + "TLSv1.2", + ], + }, + "DomainName": { + "Fn::Join": [ + "", + [ + { + "Ref": "mainapiC7DC6378", + }, + ".execute-api.ap-northeast-1.amazonaws.com", + ], + ], + }, + "Id": "TestInfraStackhttpdistributionOrigin13C4D9587", + "OriginCustomHeaders": [ + { + "HeaderName": "X-Origin-Verify", + "HeaderValue": "dev-cdk-test-deploy-653338303031343731376464613638663933303936316338666463646537", + }, + ], + }, + ], + "PriceClass": "PriceClass_200", + "Restrictions": { + "GeoRestriction": { + "Locations": [ + "JP", + "VN", + ], + "RestrictionType": "whitelist", + }, + }, + "ViewerCertificate": { + "AcmCertificateArn": "arn:aws:acm:us-east-1:058264278704:certificate/668eb08d-b92f-488c-9e32-56854d487315", + "MinimumProtocolVersion": "TLSv1.2_2021", + "SslSupportMethod": "sni-only", + }, + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::CloudFront::Distribution", + }, + "httpdistributionarecord0FBAAC08": { + "Properties": { + "HostedZoneId": "Z083753814XLAICIY5OHU", + "Name": "dev-api-cdk.mbc-cqrs-serverless.mbc-net.com.", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "httpdistribution87A15827", + "DomainName", + ], + }, + ], + "TTL": "1800", + "Type": "CNAME", + }, + "Type": "AWS::Route53::RecordSet", + }, + "lambdaapi893CD94E": { + "DependsOn": [ + "lambdaapiServiceRoleDefaultPolicy852237F1", + "lambdaapiServiceRole7E4263EE", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-101010101010-ap-northeast-1", + "S3Key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.zip", + }, + "Environment": { + "Variables": { + "APPSYNC_ENDPOINT": { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "GraphQLUrl", + ], + }, + "APP_NAME": "cdk-test-deploy", + "ATTRIBUTE_LIMIT_SIZE": "389120", + "COGNITO_USER_POOL_ID": "ap-northeast-1_xlQVMPxtx", + "DATABASE_URL": "postgresql:///mbc/cqrs/rds-account@http://db.mbc-net.com/dev_cdk_infra?schema=public", + "EVENT_SOURCE_DISABLED": "false", + "FRONT_BASE_URL": "https://dev-front.mbc-cqrs-serverless.mbc-net.com", + "LOG_LEVEL": "verbose", + "NODE_ENV": "dev", + "NODE_OPTIONS": "--enable-source-maps", + "S3_BUCKET_NAME": { + "Ref": "ddbattributes95C63151", + }, + "S3_PUBLIC_BUCKET_NAME": { + "Ref": "publicbucket0D82CFFB", + }, + "SES_FROM_EMAIL": "noreply@mbc-cqrs-serverless.mbc-net.com", + "SFN_COMMAND_ARN": "arn:aws:states:ap-northeast-1:101010101010:stateMachine:dev-cdk-test-deploy-command-handler", + "SNS_TOPIC_ARN": { + "Ref": "mainsnsC0381B34", + }, + "TZ": "Asia/Tokyo", + }, + }, + "FunctionName": "dev-cdk-test-deploy-lambda-api", + "Handler": "main.handler", + "Layers": [ + { + "Ref": "mainlayer908FE5E4", + }, + ], + "LoggingConfig": { + "ApplicationLogLevel": "TRACE", + "LogFormat": "JSON", + "SystemLogLevel": "DEBUG", + }, + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "lambdaapiServiceRole7E4263EE", + "Arn", + ], + }, + "Runtime": "nodejs18.x", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + "Timeout": 31536000, + "TracingConfig": { + "Mode": "Active", + }, + "VpcConfig": { + "SecurityGroupIds": [ + "sg-0913ccd0827f688f4", + ], + "SubnetIds": [], + }, + }, + "Type": "AWS::Lambda::Function", + }, + "lambdaapiDynamoDBEventSourceTestInfraStackmastercommandtableE6C83C78574B619C": { + "Properties": { + "BatchSize": 1, + "EventSourceArn": { + "Fn::GetAtt": [ + "mastercommanddecsC637D29D", + "Table.LatestStreamArn", + ], + }, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{"eventName":["INSERT"]}", + }, + ], + }, + "FunctionName": { + "Ref": "lambdaapi893CD94E", + }, + "StartingPosition": "TRIM_HORIZON", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "lambdaapiDynamoDBEventSourceTestInfraStacktaskstable708E175752ADBCFE": { + "Properties": { + "BatchSize": 1, + "EventSourceArn": { + "Fn::GetAtt": [ + "tasksdecs1B0120D7", + "Table.LatestStreamArn", + ], + }, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{"eventName":["INSERT"]}", + }, + ], + }, + "FunctionName": { + "Ref": "lambdaapi893CD94E", + }, + "StartingPosition": "TRIM_HORIZON", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "lambdaapiServiceRole7E4263EE": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", + ], + ], + }, + ], + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "lambdaapiServiceRoleDefaultPolicy852237F1": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "tasksqs12D9B615", + "Arn", + ], + }, + }, + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "notifysqsEC095A61", + "Arn", + ], + }, + }, + { + "Action": "dynamodb:ListStreams", + "Effect": "Allow", + "Resource": "*", + }, + { + "Action": [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "tasksdecs1B0120D7", + "Table.LatestStreamArn", + ], + }, + }, + { + "Action": [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "mastercommanddecsC637D29D", + "Table.LatestStreamArn", + ], + }, + }, + { + "Action": [ + "cognito-idp:AdminGetUser", + "cognito-idp:AdminAddUserToGroup", + "cognito-idp:AdminCreateUser", + "cognito-idp:AdminDeleteUser", + "cognito-idp:AdminDisableUser", + "cognito-idp:AdminEnableUser", + "cognito-idp:AdminSetUserPassword", + "cognito-idp:AdminResetUserPassword", + "cognito-idp:AdminUpdateUserAttributes", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":cognito-idp:ap-northeast-1:101010101010:userpool/ap-northeast-1_xlQVMPxtx", + ], + ], + }, + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ddbattributes95C63151", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ddbattributes95C63151", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "publicbucket0D82CFFB", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "publicbucket0D82CFFB", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "mainsnsC0381B34", + }, + }, + { + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "tasksqs12D9B615", + "Arn", + ], + }, + }, + { + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "notifysqsEC095A61", + "Arn", + ], + }, + }, + { + "Action": "appsync:GraphQL", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":appsync:ap-northeast-1:101010101010:apis/", + { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "ApiId", + ], + }, + "/types/Mutation/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaapiServiceRoleDefaultPolicy852237F1", + "Roles": [ + { + "Ref": "lambdaapiServiceRole7E4263EE", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdaapiSqsEventSourceTestInfraStacknotifysqs3E7CCF8682B43C89": { + "Properties": { + "BatchSize": 1, + "EventSourceArn": { + "Fn::GetAtt": [ + "notifysqsEC095A61", + "Arn", + ], + }, + "FunctionName": { + "Ref": "lambdaapi893CD94E", + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "lambdaapiSqsEventSourceTestInfraStacktasksqsE3E6B075D9265AC4": { + "Properties": { + "BatchSize": 1, + "EventSourceArn": { + "Fn::GetAtt": [ + "tasksqs12D9B615", + "Arn", + ], + }, + "FunctionName": { + "Ref": "lambdaapi893CD94E", + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "lambdaapiddbpolicyCE9EA2DA": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:GetItem", + "dynamodb:Query", + ], + "Effect": "Allow", + "Resource": "arn:aws:dynamodb:ap-northeast-1:101010101010:table/dev-cdk-test-deploy-*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaapiddbpolicyCE9EA2DA", + "Roles": [ + { + "Ref": "lambdaapiServiceRole7E4263EE", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdaapissmpolicy9067745F": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:GetParameter", + "kms:Decrypt", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaapissmpolicy9067745F", + "Roles": [ + { + "Ref": "lambdaapiServiceRole7E4263EE", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdaeventsfnpolicy5F8059AB": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "states:StartExecution", + "states:GetExecutionHistory", + "states:DescribeExecution", + ], + "Effect": "Allow", + "Resource": "arn:aws:states:ap-northeast-1:101010101010:stateMachine:dev-cdk-test-deploy-command-handler", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaeventsfnpolicy5F8059AB", + "Roles": [ + { + "Ref": "lambdaapiServiceRole7E4263EE", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdasespolicyC6CB28C8": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ses:SendEmail", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdasespolicyC6CB28C8", + "Roles": [ + { + "Ref": "lambdaapiServiceRole7E4263EE", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "mainapiANYeventproxy80A3496A": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "AWS_IAM", + "RouteKey": "ANY /event/{proxy+}", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiANYeventproxymainapilambdaF98465E9": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "PayloadFormatVersion": "2.0", + }, + "Type": "AWS::ApiGatewayV2::Integration", + }, + "mainapiANYeventproxymainapilambdaPermission1B28AC42": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/event/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainapiC7DC6378": { + "Properties": { + "CorsConfiguration": { + "AllowCredentials": false, + "AllowHeaders": [ + "*", + ], + "AllowMethods": [ + "*", + ], + "AllowOrigins": [ + "*", + ], + "MaxAge": 31536000, + }, + "Description": "HTTP API for Lambda integration", + "Name": "dev-cdk-test-deploy-api", + "ProtocolType": "HTTP", + "Tags": { + "env": "dev", + "name": "cdk-test-deploy", + }, + }, + "Type": "AWS::ApiGatewayV2::Api", + }, + "mainapiCognitoAuthorizer5A8408CD": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizerType": "JWT", + "IdentitySource": [ + "$request.header.Authorization", + ], + "JwtConfiguration": { + "Audience": [ + { + "Ref": "apigwclientE7D084A5", + }, + ], + "Issuer": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xlQVMPxtx", + }, + "Name": "CognitoAuthorizer", + }, + "Type": "AWS::ApiGatewayV2::Authorizer", + }, + "mainapiDELETEproxyE859E461": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "mainapiCognitoAuthorizer5A8408CD", + }, + "RouteKey": "DELETE /{proxy+}", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiDELETEproxymainapilambdaPermission7153CDD2": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainapiDefaultStageA012029E": { + "Properties": { + "AccessLogSettings": { + "DestinationArn": { + "Fn::GetAtt": [ + "httpapiAccessLogs0179ABB1", + "Arn", + ], + }, + "Format": "{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","userAgent":"$context.identity.userAgent","sourceIp":"$context.identity.sourceIp","requestTime":"$context.requestTime","requestTimeEpoch":"$context.requestTimeEpoch","httpMethod":"$context.httpMethod","routeKey":"$context.routeKey","path":"$context.path","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","domainName":"$context.domainName","responseLatency":"$context.responseLatency","integrationLatency":"$context.integrationLatency","username":"$context.authorizer.claims.sub"}", + }, + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AutoDeploy": true, + "DefaultRouteSettings": { + "DetailedMetricsEnabled": true, + }, + "StageName": "$default", + "Tags": { + "env": "dev", + "name": "cdk-test-deploy", + }, + }, + "Type": "AWS::ApiGatewayV2::Stage", + }, + "mainapiGET64CF2802": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "NONE", + "RouteKey": "GET /", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiGETmainapilambdaPermissionFB7DDD53": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainapiGETproxy81BAB755": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "mainapiCognitoAuthorizer5A8408CD", + }, + "RouteKey": "GET /{proxy+}", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiGETproxymainapilambdaPermission8C294959": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainapiHEADproxyA69C7EE3": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "mainapiCognitoAuthorizer5A8408CD", + }, + "RouteKey": "HEAD /{proxy+}", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiHEADproxymainapilambdaPermissionE948759F": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainapiPATCHproxy3C068B51": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "mainapiCognitoAuthorizer5A8408CD", + }, + "RouteKey": "PATCH /{proxy+}", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiPATCHproxymainapilambdaPermission6885C849": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainapiPOSTproxy1AEEF7DB": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "mainapiCognitoAuthorizer5A8408CD", + }, + "RouteKey": "POST /{proxy+}", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiPOSTproxymainapilambdaPermission3366662E": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainapiPUTproxyF7A0BCF5": { + "Properties": { + "ApiId": { + "Ref": "mainapiC7DC6378", + }, + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "mainapiCognitoAuthorizer5A8408CD", + }, + "RouteKey": "PUT /{proxy+}", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mainapiANYeventproxymainapilambdaF98465E9", + }, + ], + ], + }, + }, + "Type": "AWS::ApiGatewayV2::Route", + }, + "mainapiPUTproxymainapilambdaPermissionC9953BE1": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambdaapi893CD94E", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:ap-northeast-1:101010101010:", + { + "Ref": "mainapiC7DC6378", + }, + "/*/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "mainlayer908FE5E4": { + "Properties": { + "CompatibleArchitectures": [ + "arm64", + ], + "CompatibleRuntimes": [ + "nodejs18.x", + ], + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-101010101010-ap-northeast-1", + "S3Key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.zip", + }, + "LayerName": "dev-cdk-test-deploy-main-layer", + }, + "Type": "AWS::Lambda::LayerVersion", + }, + "mainsnsC0381B34": { + "Properties": { + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + "TopicName": "dev-cdk-test-deploy-main-sns", + }, + "Type": "AWS::SNS::Topic", + }, + "mastercommanddecsC637D29D": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "mastercommanddecsCustomResourcePolicyE2063016", + ], + "Properties": { + "Create": "{"service":"DynamoDB","action":"describeTable","parameters":{"TableName":"dev-cdk-test-deploy-master-command"},"physicalResourceId":{"responsePath":"Table.TableArn"}}", + "InstallLatestAwsSdk": true, + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn", + ], + }, + }, + "Type": "Custom::AWS", + "UpdateReplacePolicy": "Delete", + }, + "mastercommanddecsCustomResourcePolicyE2063016": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:DescribeTable", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "mastercommanddecsCustomResourcePolicyE2063016", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "notifysqsEC095A61": { + "DeletionPolicy": "Delete", + "Properties": { + "QueueName": "dev-cdk-test-deploy-notification-queue", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + }, + "notifysqsPolicy96E4066E": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "mainsnsC0381B34", + }, + }, + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com", + }, + "Resource": { + "Fn::GetAtt": [ + "notifysqsEC095A61", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "Queues": [ + { + "Ref": "notifysqsEC095A61", + }, + ], + }, + "Type": "AWS::SQS::QueuePolicy", + }, + "notifysqsTestInfraStackmainsnsA010224C9104B89F": { + "DependsOn": [ + "notifysqsPolicy96E4066E", + ], + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "notifysqsEC095A61", + "Arn", + ], + }, + "FilterPolicy": { + "action": [ + "command-status", + "task-status", + ], + }, + "Protocol": "sqs", + "RawMessageDelivery": true, + "TopicArn": { + "Ref": "mainsnsC0381B34", + }, + }, + "Type": "AWS::SNS::Subscription", + }, + "publicbucket0D82CFFB": { + "DeletionPolicy": "Delete", + "Properties": { + "BucketName": "dev-cdk-test-deploy-public", + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedHeaders": [ + "*", + ], + "AllowedMethods": [ + "GET", + "PUT", + "POST", + ], + "AllowedOrigins": [ + "*", + ], + "MaxAge": 3000, + }, + ], + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + }, + "publicbucketOAI56D47DD1": { + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Allows CloudFront to reach the bucket", + }, + }, + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + }, + "publicbucketPolicy3A0184E3": { + "Properties": { + "Bucket": { + "Ref": "publicbucket0D82CFFB", + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": { + "CanonicalUser": { + "Fn::GetAtt": [ + "publicbucketOAI56D47DD1", + "S3CanonicalUserId", + ], + }, + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "publicbucket0D82CFFB", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "publicbucketdistribution7A24D15F": { + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD", + ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "CachedMethods": [ + "GET", + "HEAD", + ], + "Compress": true, + "TargetOriginId": "TestInfraStackpublicbucketdistributionOrigin129EB7A5E", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "publicbucket0D82CFFB", + "RegionalDomainName", + ], + }, + "Id": "TestInfraStackpublicbucketdistributionOrigin129EB7A5E", + "S3OriginConfig": { + "OriginAccessIdentity": { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + "Ref": "publicbucketOAI56D47DD1", + }, + ], + ], + }, + }, + }, + ], + "PriceClass": "PriceClass_200", + "Restrictions": { + "GeoRestriction": { + "Locations": [ + "JP", + "VN", + ], + "RestrictionType": "whitelist", + }, + }, + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::CloudFront::Distribution", + }, + "realtime0CF38FF2": { + "Properties": { + "AdditionalAuthenticationProviders": [ + { + "AuthenticationType": "AWS_IAM", + }, + { + "AuthenticationType": "AMAZON_COGNITO_USER_POOLS", + "UserPoolConfig": { + "AwsRegion": "ap-northeast-1", + "UserPoolId": "ap-northeast-1_xlQVMPxtx", + }, + }, + ], + "AuthenticationType": "API_KEY", + "Name": "dev-cdk-test-deploy-realtime", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + "XrayEnabled": true, + }, + "Type": "AWS::AppSync::GraphQLApi", + }, + "realtimeDefaultApiKeyBD0EC5CB": { + "DependsOn": [ + "realtimeSchema7B454650", + ], + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "ApiId", + ], + }, + "Expires": 1762419954, + }, + "Type": "AWS::AppSync::ApiKey", + }, + "realtimeDomainAssociationD479792F": { + "DependsOn": [ + "realtimeDomainName01674B94", + ], + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "ApiId", + ], + }, + "DomainName": "dev-appsync-cdk.mbc-cqrs-serverless.mbc-net.com", + }, + "Type": "AWS::AppSync::DomainNameApiAssociation", + }, + "realtimeDomainName01674B94": { + "Properties": { + "CertificateArn": "arn:aws:acm:us-east-1:058264278704:certificate/668eb08d-b92f-488c-9e32-56854d487315", + "Description": { + "Fn::Join": [ + "", + [ + "domain for dev-cdk-test-deploy-realtime at ", + { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "GraphQLUrl", + ], + }, + ], + ], + }, + "DomainName": "dev-appsync-cdk.mbc-cqrs-serverless.mbc-net.com", + }, + "Type": "AWS::AppSync::DomainName", + }, + "realtimeNoneDataSource2E50F7A6": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "ApiId", + ], + }, + "Name": "NoneDataSource", + "Type": "NONE", + }, + "Type": "AWS::AppSync::DataSource", + }, + "realtimeSchema7B454650": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "ApiId", + ], + }, + "Definition": "type Message @aws_api_key @aws_iam @aws_cognito_user_pools @aws_oidc { + id: String! + table: String! + pk: String! + sk: String! + tenantCode: String! + action: String! + content: AWSJSON! +} + +type Query { + getMessage(id: String!): Message +} + +type Mutation { + sendMessage(message: AWSJSON!): Message! @aws_iam +} + +type Subscription { + onMessage(tenantCode: String!, action: String, id: String): Message + @aws_subscribe(mutations: ["sendMessage"]) + @aws_api_key + @aws_iam + @aws_cognito_user_pools + @aws_oidc +} + +schema { + query: Query + mutation: Mutation + subscription: Subscription +} +", + }, + "Type": "AWS::AppSync::GraphQLSchema", + }, + "realtimesendMessageResolverA93F264B": { + "DependsOn": [ + "realtimeNoneDataSource2E50F7A6", + "realtimeSchema7B454650", + ], + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "realtime0CF38FF2", + "ApiId", + ], + }, + "DataSourceName": "NoneDataSource", + "FieldName": "sendMessage", + "Kind": "UNIT", + "RequestMappingTemplate": "{"version": "2018-05-29","payload": $util.toJson($context.arguments.message)}", + "ResponseMappingTemplate": "$util.toJson($context.result)", + "TypeName": "Mutation", + }, + "Type": "AWS::AppSync::Resolver", + }, + "taskdeadlettersqs91D16094": { + "DeletionPolicy": "Delete", + "Properties": { + "QueueName": "dev-cdk-test-deploy-task-dead-letter-queue", + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + }, + "taskdeadlettersqsPolicy9CAEB195": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "alarmsnsFB7BBC3B", + }, + }, + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com", + }, + "Resource": { + "Fn::GetAtt": [ + "taskdeadlettersqs91D16094", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "Queues": [ + { + "Ref": "taskdeadlettersqs91D16094", + }, + ], + }, + "Type": "AWS::SQS::QueuePolicy", + }, + "taskdeadlettersqsTestInfraStackalarmsnsB0FD064D9665A5CE": { + "DependsOn": [ + "taskdeadlettersqsPolicy9CAEB195", + ], + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "taskdeadlettersqs91D16094", + "Arn", + ], + }, + "Protocol": "sqs", + "RawMessageDelivery": true, + "TopicArn": { + "Ref": "alarmsnsFB7BBC3B", + }, + }, + "Type": "AWS::SNS::Subscription", + }, + "tasksdecs1B0120D7": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "tasksdecsCustomResourcePolicy766680A1", + ], + "Properties": { + "Create": "{"service":"DynamoDB","action":"describeTable","parameters":{"TableName":"dev-cdk-test-deploy-tasks"},"physicalResourceId":{"responsePath":"Table.TableArn"}}", + "InstallLatestAwsSdk": true, + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn", + ], + }, + }, + "Type": "Custom::AWS", + "UpdateReplacePolicy": "Delete", + }, + "tasksdecsCustomResourcePolicy766680A1": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:DescribeTable", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "tasksdecsCustomResourcePolicy766680A1", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "tasksqs12D9B615": { + "DeletionPolicy": "Delete", + "Properties": { + "QueueName": "dev-cdk-test-deploy-task-action-queue", + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "taskdeadlettersqs91D16094", + "Arn", + ], + }, + "maxReceiveCount": 5, + }, + "Tags": [ + { + "Key": "env", + "Value": "dev", + }, + { + "Key": "name", + "Value": "cdk-test-deploy", + }, + ], + }, + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + }, + "tasksqsPolicyF1D0C5F5": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "mainsnsC0381B34", + }, + }, + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com", + }, + "Resource": { + "Fn::GetAtt": [ + "tasksqs12D9B615", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "Queues": [ + { + "Ref": "tasksqs12D9B615", + }, + ], + }, + "Type": "AWS::SQS::QueuePolicy", + }, + "tasksqsTestInfraStackmainsnsA010224CA61CC66C": { + "DependsOn": [ + "tasksqsPolicyF1D0C5F5", + ], + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "tasksqs12D9B615", + "Arn", + ], + }, + "FilterPolicy": { + "action": [ + "task-execute", + ], + }, + "Protocol": "sqs", + "RawMessageDelivery": true, + "TopicArn": { + "Ref": "mainsnsC0381B34", + }, + }, + "Type": "AWS::SNS::Subscription", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; diff --git a/packages/cli/templates/infra/test/infra.test.ts b/packages/cli/templates/infra/test/infra.test.ts new file mode 100644 index 0000000..9c622e5 --- /dev/null +++ b/packages/cli/templates/infra/test/infra.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) Murakami Business Consulting, Inc. All rights are reserved. + */ + +import * as cdk from 'aws-cdk-lib' +import { Template } from 'aws-cdk-lib/assertions' +import { getConfig } from '../config' +import { InfraStack } from '../libs/infra-stack' + +jest.mock('crypto', () => ({ + ...jest.requireActual('crypto'), + randomBytes: jest.fn(() => Buffer.from('e380014717dda68f930961c8fdcde7')), +})) + +jest.mock('aws-cdk-lib', () => ({ + ...jest.requireActual('aws-cdk-lib'), + Duration: { + days: jest.fn(() => ({ + toMilliseconds: jest.fn(() => 365 * 24 * 60 * 60 * 1000), // Mock milliseconds for 365 days + })), + hours: jest.fn(() => ({ + minutes: jest.fn(() => 365 * 24 * 60), + toSeconds: jest.fn(() => 365 * 24 * 60 * 60), + })), + minutes: jest.fn(() => ({ + toSeconds: jest.fn(() => 365 * 24 * 60), + })), + seconds: jest.fn(() => ({ + toSeconds: jest.fn(() => 365 * 24 * 60 * 60), + })), + }, + Expiration: { + after: jest.fn(() => ({ + isBefore: jest.fn(() => false), + isAfter: jest.fn(() => false), + toEpoch: jest.fn(() => 1762419954), + })), + }, +})) + +function replaceKeyValue(obj: any, desKey: string, desVal: string): any { + if (typeof obj !== 'object' || obj === null) { + return obj + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (key === desKey) { + obj[key] = desVal + } else if (typeof obj[key] === 'object') { + obj[key] = replaceKeyValue(obj[key], desKey, desVal) + } + } + } + + return obj +} + +test('snapshot test for InfraStack', () => { + const cdkEnv: cdk.Environment = { + account: '101010101010', + region: 'ap-northeast-1', + } + const config = getConfig('dev') + const app = new cdk.App() + const stack = new InfraStack(app, 'TestInfraStack', { env: cdkEnv, config }) + let template = Template.fromStack(stack).toJSON() + template = replaceKeyValue( + template, + 'S3Key', + `${Array(64).fill('x').join('')}.zip`, + ) + template = replaceKeyValue( + template, + 'Fn::Sub', + '101010101010.dkr.ecr.xxxxxxxxxxxx.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-101010101010-xxxxxxxxxxxx:' + + Array(64).fill('x').join(''), + ) + + expect(template).toMatchSnapshot() +}) diff --git a/packages/cli/templates/infra/tsconfig.json b/packages/cli/templates/infra/tsconfig.json new file mode 100644 index 0000000..5a88e4c --- /dev/null +++ b/packages/cli/templates/infra/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020", "dom"], + "declaration": false, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} diff --git a/packages/cli/templates/jest.config.js b/packages/cli/templates/jest.config.js deleted file mode 100644 index 4a5b465..0000000 --- a/packages/cli/templates/jest.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; diff --git a/packages/cli/templates/jest.config.json b/packages/cli/templates/jest.config.json new file mode 100644 index 0000000..d1f82bb --- /dev/null +++ b/packages/cli/templates/jest.config.json @@ -0,0 +1,19 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": "\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "^test/(.*)$": "/test/$1", + "^src/(.*)$": "/src/$1" + }, + "modulePathIgnorePatterns": ["infra"], + "passWithNoTests": true, + "reporters": [ + "default", + ["jest-junit", { "outputDirectory": "report", "outputName": "unit.xml" }] + ] +} diff --git a/packages/cli/templates/package.json b/packages/cli/templates/package.json index 9c73a2a..a37eae8 100644 --- a/packages/cli/templates/package.json +++ b/packages/cli/templates/package.json @@ -11,7 +11,10 @@ "build": "nest build --watch", "build:prod": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", "start:repl": "nest start --watch --entryFile repl", + "start:prod": "node dist/main", "offline:docker:build": "run-script-os", "offline:docker:build:default": "cd infra-local && docker compose up --build --remove-orphans", "offline:docker:build:win32": "powershell -Command \"Set-Location infra-local; docker compose up --build --remove-orphans\"", @@ -37,21 +40,10 @@ }, "dependencies": { "@mbc-cqrs-serverless/core": "", - "@prisma/client": "^5.7.1" + "@prisma/client": "^5.7.1", + "prisma": "^5.7.1" }, "devDependencies": { - "@aws-sdk/client-dynamodb": "^3.478.0", - "@aws-sdk/client-s3": "^3.478.0", - "@aws-sdk/client-sesv2": "^3.478.0", - "@aws-sdk/client-sfn": "^3.478.0", - "@aws-sdk/client-sns": "^3.478.0", - "@aws-sdk/client-sqs": "^3.478.0", - "@aws-sdk/credential-provider-node": "^3.451.0", - "@aws-sdk/lib-storage": "^3.478.0", - "@aws-sdk/s3-request-presigner": "^3.478.0", - "@aws-sdk/signature-v4": "^3.374.0", - "@aws-sdk/util-create-request": "^3.468.0", - "@aws-sdk/util-dynamodb": "^3.360.0", "@mbc-cqrs-serverless/cli": "", "@nestjs/cli": "^10.2.1", "@nestjs/common": "^10.3.0", @@ -76,9 +68,9 @@ "eslint-plugin-prettier": "^5.1.2", "eslint-plugin-simple-import-sort": "^10.0.0", "jest": "^29.7.0", + "jest-junit": "^16.0.0", "nestjs-spelunker": "^1.3.0", "prettier": "^3.1.1", - "prisma": "^5.7.1", "run-script-os": "^1.1.6", "serverless": "^3.38.0", "serverless-dynamodb": "^0.2.47", @@ -109,21 +101,19 @@ "ulid": "^2.3.0", "webpack": "^5.88.2" }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" + "optionalDependencies": { + "@aws-sdk/client-dynamodb": "^3.606.0", + "@aws-sdk/client-s3": "^3.608.0", + "@aws-sdk/client-sesv2": "^3.608.0", + "@aws-sdk/client-sfn": "^3.606.0", + "@aws-sdk/client-sns": "^3.606.0", + "@aws-sdk/client-sqs": "^3.606.0", + "@aws-sdk/client-ssm": "^3.606.0", + "@aws-sdk/credential-provider-node": "^3.600.0", + "@aws-sdk/lib-storage": "^3.608.0", + "@aws-sdk/s3-request-presigner": "^3.608.0", + "@aws-sdk/signature-v4": "^3.374.0", + "@aws-sdk/util-create-request": "^3.598.0", + "@aws-sdk/util-dynamodb": "^3.606.0" } } diff --git a/packages/core/README.md b/packages/core/README.md index 62fdc72..cddedd3 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,10 +1,12 @@ -![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png) +![MBC CQRS serverless framework](https://mbc-cqrs-serverless.mbc-net.com/img/mbc-cqrs-serverless.png) + # MBC CQRS serverless framework CORE package ## Documentation -Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation. +Visit https://mbc-cqrs-serverless.mbc-net.com/ to view the full documentation. ## License + Copyright © 2024, Murakami Business Consulting, Inc. https://www.mbc-net.com/ This project and sub projects are under the MIT License. diff --git a/packages/core/package.json b/packages/core/package.json index bad136a..0d4011f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/core", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "description": "CQRS and event base core", "keywords": [ "mbc", @@ -45,7 +45,7 @@ "bugs": { "url": "https://github.com/mbc-net/mbc-cqrs-serverless/issues" }, - "homepage": "https://mbc-net.github.io/mbc-cqrs-serverless-doc/", + "homepage": "https://mbc-cqrs-serverless.mbc-net.com/", "publishConfig": { "access": "public" }, diff --git a/packages/core/test/e2e/publish.e2e-spec.ts b/packages/core/test/e2e/publish.e2e-spec.ts index 8a89e27..2aa347c 100644 --- a/packages/core/test/e2e/publish.e2e-spec.ts +++ b/packages/core/test/e2e/publish.e2e-spec.ts @@ -7,15 +7,15 @@ import { syncDataFinished } from './utils' const API_PATH = '/api/testing' describe('Publish', () => { - it('should be stored correct data in DDB', async () => { + it('should be stored correct data in the command DDB table', async () => { // Arrange const payload = { pk: 'TEST', - sk: 'publish', - id: 'TEST#publish', - name: 'testing', + sk: 'publish#command', + id: 'TEST#publish#command', + name: 'testing#command', version: 0, - code: 'publish', + code: 'publish#command', type: 'TEST', } @@ -25,16 +25,93 @@ describe('Publish', () => { // Assert expect(res.statusCode).toEqual(201) - await syncDataFinished('testing-table', { pk: 'TEST', sk: 'publish@1' }) + await syncDataFinished('testing-table', { + pk: payload.pk, + sk: `${payload.sk}@1`, + }) - const data = await getItem(getTableName('testing-table', TableType.DATA), { + const data = await getItem( + getTableName('testing-table', TableType.COMMAND), + { + pk: payload.pk, + sk: `${payload.sk}@1`, + }, + ) + + console.log('data', data) + + expect(data).toMatchObject({ + ...payload, + version: 1, + sk: `${payload.sk}@1`, + }) + }, 40000) + + it('should be stored correct data in the data DDB table', async () => { + // Arrange + const payload = { pk: 'TEST', - sk: 'publish', + sk: 'publish#data', + id: 'TEST#publish#data', + name: 'testing#data', + version: 0, + code: 'publish#data', + type: 'TEST', + } + + // Action + const res = await request(config.apiBaseUrl).post(API_PATH).send(payload) + + // Assert + expect(res.statusCode).toEqual(201) + + await syncDataFinished('testing-table', { + pk: payload.pk, + sk: `${payload.sk}@1`, + }) + + const data = await getItem(getTableName('testing-table', TableType.DATA), { + pk: payload.pk, + sk: payload.sk, }) expect(data).toMatchObject({ ...payload, version: 1 }) }, 40000) + it('should be stored correct data in the history DDB table', async () => { + // Arrange + const payload = { + pk: 'TEST', + sk: 'publish#history', + id: 'TEST#publish#history', + name: 'testing#history', + version: 0, + code: 'publish#history', + type: 'TEST', + } + + // Action + const res = await request(config.apiBaseUrl).post(API_PATH).send(payload) + + await syncDataFinished('testing-table', { + pk: payload.pk, + sk: `${payload.sk}@1`, + }) + + // Assert + expect(res.statusCode).toEqual(201) + + const data = await getItem( + getTableName('testing-table', TableType.HISTORY), + { + pk: payload.pk, + sk: `${payload.sk}@1`, + }, + ) + + expect(data).toBeUndefined() + }, 40000) + it('should return invalid input version', async () => { // Arrange const payload = { @@ -71,7 +148,10 @@ describe('Publish', () => { await request(config.apiBaseUrl).post(API_PATH).send(payload) - await syncDataFinished('testing-table', { pk: 'TEST', sk: 'publish_2@1' }) + await syncDataFinished('testing-table', { + pk: payload.pk, + sk: `${payload.sk}@1`, + }) const res = await request(config.apiBaseUrl).post(API_PATH).send(payload) diff --git a/packages/sequence/README.md b/packages/sequence/README.md index e38bf77..627d13f 100644 --- a/packages/sequence/README.md +++ b/packages/sequence/README.md @@ -1,10 +1,12 @@ -![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png) +![MBC CQRS serverless framework](https://mbc-cqrs-serverless.mbc-net.com/img/mbc-cqrs-serverless.png) + # MBC CQRS serverless framework Sequence package ## Documentation -Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation. +Visit https://mbc-cqrs-serverless.mbc-net.com/ to view the full documentation. ## License + Copyright © 2024, Murakami Business Consulting, Inc. https://www.mbc-net.com/ This project and sub projects are under the MIT License. diff --git a/packages/sequence/package.json b/packages/sequence/package.json index 8e3ed09..a436c33 100644 --- a/packages/sequence/package.json +++ b/packages/sequence/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/sequence", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "description": "Generate increment sequence with time-rotation", "keywords": [ "mbc", @@ -36,11 +36,11 @@ "bugs": { "url": "https://github.com/mbc-net/mbc-cqrs-serverless/issues" }, - "homepage": "https://mbc-net.github.io/mbc-cqrs-serverless-doc/", + "homepage": "https://mbc-cqrs-serverless.mbc-net.com/", "publishConfig": { "access": "public" }, "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.30-beta.0" } } diff --git a/packages/task/README.md b/packages/task/README.md index 3782e8b..d6a3db3 100644 --- a/packages/task/README.md +++ b/packages/task/README.md @@ -1,10 +1,12 @@ -![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png) +![MBC CQRS serverless framework](https://mbc-cqrs-serverless.mbc-net.com/img/mbc-cqrs-serverless.png) + # MBC CQRS serverless framework Task package ## Documentation -Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation. +Visit https://mbc-cqrs-serverless.mbc-net.com/ to view the full documentation. ## License + Copyright © 2024, Murakami Business Consulting, Inc. https://www.mbc-net.com/ This project and sub projects are under the MIT License. diff --git a/packages/task/package.json b/packages/task/package.json index ccffffb..6637a0a 100644 --- a/packages/task/package.json +++ b/packages/task/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/task", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "description": "long-running task", "keywords": [ "mbc", @@ -36,11 +36,11 @@ "bugs": { "url": "https://github.com/mbc-net/mbc-cqrs-serverless/issues" }, - "homepage": "https://mbc-net.github.io/mbc-cqrs-serverless-doc/", + "homepage": "https://mbc-cqrs-serverless.mbc-net.com/", "publishConfig": { "access": "public" }, "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.30-beta.0" } } diff --git a/packages/ui-setting/README.md b/packages/ui-setting/README.md index dfae003..4939f25 100644 --- a/packages/ui-setting/README.md +++ b/packages/ui-setting/README.md @@ -1,10 +1,12 @@ -![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png) +![MBC CQRS serverless framework](https://mbc-cqrs-serverless.mbc-net.com/img/mbc-cqrs-serverless.png) + # MBC CQRS serverless framework UI Setting package ## Documentation -Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation. +Visit https://mbc-cqrs-serverless.mbc-net.com/ to view the full documentation. ## License + Copyright © 2024, Murakami Business Consulting, Inc. https://www.mbc-net.com/ This project and sub projects are under the MIT License. diff --git a/packages/ui-setting/package.json b/packages/ui-setting/package.json index 4e638fc..b68eb49 100644 --- a/packages/ui-setting/package.json +++ b/packages/ui-setting/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/ui-setting", - "version": "0.1.21-beta.0", + "version": "0.1.30-beta.0", "description": "Setting master data", "keywords": [ "mbc", @@ -36,11 +36,11 @@ "bugs": { "url": "https://github.com/mbc-net/mbc-cqrs-serverless/issues" }, - "homepage": "https://mbc-net.github.io/mbc-cqrs-serverless-doc/", + "homepage": "https://mbc-cqrs-serverless.mbc-net.com/", "publishConfig": { "access": "public" }, "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.21-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.30-beta.0" } }