diff --git a/.gitignore b/.gitignore index 16e57b6..18c8917 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ jspm_packages/ *.tgz .yarn-integrity .cache +bin/config.json +pages/.vitepress/dist/* /test-reports/ junit.xml /coverage/ @@ -40,7 +42,7 @@ junit.xml !/test/ !/tsconfig.json !/tsconfig.dev.json -!/src/ +!/./ /lib /dist/ !/.eslintrc.json diff --git a/.npmignore b/.npmignore index 2209108..c66bf2b 100644 --- a/.npmignore +++ b/.npmignore @@ -7,7 +7,7 @@ permissions-backup.acl /.mergify.yml /test/ /tsconfig.dev.json -/src/ +/./ !/lib/ !/lib/**/*.js !/lib/**/*.d.ts diff --git a/.projen/deps.json b/.projen/deps.json index 7d18ef6..63630ec 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -1,9 +1,17 @@ { "dependencies": [ + { + "name": "@types/ansi-escape-sequences", + "type": "build" + }, { "name": "@types/cheerio", "type": "build" }, + { + "name": "@types/figlet", + "type": "build" + }, { "name": "@types/jest", "type": "build" @@ -35,6 +43,10 @@ "version": "^2.164.1", "type": "build" }, + { + "name": "aws-lambda", + "type": "build" + }, { "name": "cz-conventional-changelog", "type": "build" @@ -93,6 +105,10 @@ "name": "typescript", "type": "build" }, + { + "name": "vitepress", + "type": "build" + }, { "name": "@aws-amplify/cli", "type": "runtime" @@ -101,6 +117,10 @@ "name": "@aws-appsync/utils", "type": "runtime" }, + { + "name": "@aws-cdk/aws-cognito-identitypool-alpha", + "type": "runtime" + }, { "name": "@aws-lambda-powertools/logger", "type": "runtime" @@ -157,10 +177,26 @@ "name": "@aws-sdk/util-dynamodb", "type": "runtime" }, + { + "name": "@graphql-codegen/cli", + "type": "runtime" + }, + { + "name": "@graphql-codegen/plugin-helpers", + "type": "runtime" + }, { "name": "@middy/core", "type": "runtime" }, + { + "name": "@react-email/components", + "type": "runtime" + }, + { + "name": "@react-email/render", + "type": "runtime" + }, { "name": "ansi-escape-sequences", "type": "runtime" @@ -195,6 +231,14 @@ "version": "^10.0.5", "type": "runtime" }, + { + "name": "esbuild", + "type": "runtime" + }, + { + "name": "figlet", + "type": "runtime" + }, { "name": "graphql", "type": "runtime" @@ -203,6 +247,10 @@ "name": "mui-color-input", "type": "runtime" }, + { + "name": "prompt-sync", + "type": "runtime" + }, { "name": "react", "type": "runtime" @@ -219,6 +267,10 @@ "name": "tsx", "type": "runtime" }, + { + "name": "typescript", + "type": "runtime" + }, { "name": "uuid", "type": "runtime" diff --git a/.projen/tasks.json b/.projen/tasks.json index 524d94d..338909b 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -24,6 +24,15 @@ } ] }, + "build:appsync": { + "name": "build:appsync", + "description": "Build the appsync", + "steps": [ + { + "exec": "tsx ./lib/api/functions/bundle.ts" + } + ] + }, "bundle": { "name": "bundle", "description": "Prepare assets" @@ -60,10 +69,60 @@ ], "condition": "git diff --exit-code > /dev/null" }, + "codegen": { + "name": "codegen", + "description": "Run the codegen tasks", + "steps": [ + { + "spawn": "codegen:api" + }, + { + "spawn": "codegen:auth" + } + ] + }, + "codegen:api": { + "name": "codegen:api", + "description": "Generate the API code", + "steps": [ + { + "exec": "npx @aws-amplify/cli codegen" + } + ], + "cwd": "lib/shared/api" + }, + "codegen:auth": { + "name": "codegen:auth", + "description": "Generate the Auth code", + "steps": [ + { + "exec": "npx graphql-codegen ./codegen.ts" + } + ], + "cwd": "lib/shared/api/schema-to-avp" + }, + "commit": { + "name": "commit", + "description": "Commit the code", + "steps": [ + { + "exec": "git-cz" + } + ] + }, "compile": { "name": "compile", "description": "Only compile" }, + "config": { + "name": "config", + "description": "Run the CLI to configure the project", + "steps": [ + { + "exec": "ts-node ./cli/config.ts" + } + ] + }, "default": { "name": "default", "description": "Synthesize project files", @@ -102,6 +161,33 @@ } ] }, + "docs:build": { + "name": "docs:build", + "description": "Build the docs", + "steps": [ + { + "exec": "vitepress build pages" + } + ] + }, + "docs:dev": { + "name": "docs:dev", + "description": "Run the docs in dev mode", + "steps": [ + { + "exec": "vitepress dev pages" + } + ] + }, + "docs:preview": { + "name": "docs:preview", + "description": "Preview the docs", + "steps": [ + { + "exec": "vitepress preview pages" + } + ] + }, "eject": { "name": "eject", "description": "Remove projen from the project", @@ -114,16 +200,35 @@ } ] }, + "email:start": { + "name": "email:start", + "description": "Start the email generator", + "steps": [ + { + "exec": "npm run start" + } + ], + "cwd": "lib/shared/email-generator" + }, "eslint": { "name": "eslint", "description": "Runs eslint against the codebase", "steps": [ { - "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", + "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ . test build-tools projenrc .projenrc.ts", "receiveArgs": true } ] }, + "format": { + "name": "format", + "description": "Format the code", + "steps": [ + { + "exec": "npm run prettier && npm run lint" + } + ] + }, "install": { "name": "install", "description": "Install project dependencies and update lockfile (non-frozen)", @@ -142,6 +247,15 @@ } ] }, + "lint": { + "name": "lint", + "description": "Lint the code", + "steps": [ + { + "exec": "eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif --fix" + } + ] + }, "package": { "name": "package", "description": "Creates the distribution package" @@ -161,7 +275,24 @@ }, "pre-compile": { "name": "pre-compile", - "description": "Prepare the project for compilation" + "description": "Prepare the project for compilation", + "steps": [ + { + "spawn": "build:appsync" + }, + { + "spawn": "compile" + } + ] + }, + "prettier": { + "name": "prettier", + "description": "Run prettier", + "steps": [ + { + "exec": "prettier --write \"**/*.{ts,tsx}\"" + } + ] }, "synth": { "name": "synth", @@ -203,6 +334,16 @@ } ] }, + "ui:start": { + "name": "ui:start", + "description": "Start the UI", + "steps": [ + { + "exec": "vite" + } + ], + "cwd": "lib/user-interface/genai-newsletter-ui/" + }, "upgrade": { "name": "upgrade", "description": "upgrade dependencies", @@ -211,13 +352,13 @@ }, "steps": [ { - "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/cheerio,@types/jest,@types/node,@types/prompt-sync,@types/uuid,cz-conventional-changelog,esbuild,eslint-import-resolver-typescript,eslint-plugin-import,figlet,git-cz,jest,prettier,projen,ts-jest,ts-node,typescript,@aws-amplify/cli,@aws-appsync/utils,@aws-lambda-powertools/logger,@aws-lambda-powertools/metrics,@aws-lambda-powertools/tracer,@aws-sdk/client-bedrock-runtime,@aws-sdk/client-cognito-identity-provider,@aws-sdk/client-dynamodb,@aws-sdk/client-lambda,@aws-sdk/client-pinpoint,@aws-sdk/client-s3,@aws-sdk/client-scheduler,@aws-sdk/client-sfn,@aws-sdk/client-verifiedpermissions,@aws-sdk/lib-storage,@aws-sdk/util-dynamodb,@middy/core,ansi-escape-sequences,aws-jwt-verify,axios,cdk-nag,cheerio,commander,graphql,mui-color-input,react,react-email,source-map-support,tsx,uuid" + "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/ansi-escape-sequences,@types/cheerio,@types/figlet,@types/jest,@types/node,@types/prompt-sync,@types/uuid,aws-lambda,cz-conventional-changelog,esbuild,eslint-import-resolver-typescript,eslint-plugin-import,figlet,git-cz,jest,prettier,projen,ts-jest,ts-node,typescript,vitepress,@aws-amplify/cli,@aws-appsync/utils,@aws-cdk/aws-cognito-identitypool-alpha,@aws-lambda-powertools/logger,@aws-lambda-powertools/metrics,@aws-lambda-powertools/tracer,@aws-sdk/client-bedrock-runtime,@aws-sdk/client-cognito-identity-provider,@aws-sdk/client-dynamodb,@aws-sdk/client-lambda,@aws-sdk/client-pinpoint,@aws-sdk/client-s3,@aws-sdk/client-scheduler,@aws-sdk/client-sfn,@aws-sdk/client-verifiedpermissions,@aws-sdk/lib-storage,@aws-sdk/util-dynamodb,@graphql-codegen/cli,@graphql-codegen/plugin-helpers,@middy/core,@react-email/components,@react-email/render,ansi-escape-sequences,aws-jwt-verify,axios,cdk-nag,cheerio,commander,graphql,mui-color-input,prompt-sync,react,react-email,source-map-support,tsx,uuid" }, { "exec": "npm install" }, { - "exec": "npm update @types/cheerio @types/jest @types/node @types/prompt-sync @types/uuid @typescript-eslint/eslint-plugin @typescript-eslint/parser aws-cdk cz-conventional-changelog esbuild eslint-import-resolver-typescript eslint-plugin-import eslint figlet git-cz jest jest-junit prettier projen ts-jest ts-node typescript @aws-amplify/cli @aws-appsync/utils @aws-lambda-powertools/logger @aws-lambda-powertools/metrics @aws-lambda-powertools/tracer @aws-sdk/client-bedrock-runtime @aws-sdk/client-cognito-identity-provider @aws-sdk/client-dynamodb @aws-sdk/client-lambda @aws-sdk/client-pinpoint @aws-sdk/client-s3 @aws-sdk/client-scheduler @aws-sdk/client-sfn @aws-sdk/client-verifiedpermissions @aws-sdk/lib-storage @aws-sdk/util-dynamodb @middy/core ansi-escape-sequences aws-cdk-lib aws-jwt-verify axios cdk-nag cheerio commander constructs graphql mui-color-input react react-email source-map-support tsx uuid" + "exec": "npm update @types/ansi-escape-sequences @types/cheerio @types/figlet @types/jest @types/node @types/prompt-sync @types/uuid @typescript-eslint/eslint-plugin @typescript-eslint/parser aws-cdk aws-lambda cz-conventional-changelog esbuild eslint-import-resolver-typescript eslint-plugin-import eslint figlet git-cz jest jest-junit prettier projen ts-jest ts-node typescript vitepress @aws-amplify/cli @aws-appsync/utils @aws-cdk/aws-cognito-identitypool-alpha @aws-lambda-powertools/logger @aws-lambda-powertools/metrics @aws-lambda-powertools/tracer @aws-sdk/client-bedrock-runtime @aws-sdk/client-cognito-identity-provider @aws-sdk/client-dynamodb @aws-sdk/client-lambda @aws-sdk/client-pinpoint @aws-sdk/client-s3 @aws-sdk/client-scheduler @aws-sdk/client-sfn @aws-sdk/client-verifiedpermissions @aws-sdk/lib-storage @aws-sdk/util-dynamodb @graphql-codegen/cli @graphql-codegen/plugin-helpers @middy/core @react-email/components @react-email/render ansi-escape-sequences aws-cdk-lib aws-jwt-verify axios cdk-nag cheerio commander constructs graphql mui-color-input prompt-sync react react-email source-map-support tsx uuid" }, { "exec": "npx projen" diff --git a/.projenrc.ts b/.projenrc.ts index cdaa136..2497632 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1,16 +1,25 @@ -import { awscdk } from 'projen'; -import { NodePackageManager } from 'projen/lib/javascript'; -import { web } from 'projen' -import { exec } from 'child_process'; +import { awscdk, typescript } from 'projen'; +import { NodePackageManager, TypeScriptJsxMode, TypeScriptModuleResolution } from 'projen/lib/javascript'; const project = new awscdk.AwsCdkTypeScriptApp({ cdkVersion: '2.164.1', + srcdir: '.', + outdir: '.', defaultReleaseBranch: 'main', name: 'generative-ai-newsletter-app', projenrcTs: true, sampleCode: false, + gitignore: [ + 'bin/config.json', + 'pages/.vitepress/dist/*' + ], + appEntrypoint: 'bin/genai-newsletter-app.ts', + bin: { + 'genai-newsletter-app': 'bin/genai-newsletter-app.ts', + }, packageManager: NodePackageManager.NPM, deps: [ + '@aws-cdk/aws-cognito-identitypool-alpha', '@aws-sdk/client-s3', '@aws-sdk/client-cognito-identity-provider', '@aws-sdk/client-lambda', @@ -36,50 +45,199 @@ const project = new awscdk.AwsCdkTypeScriptApp({ 'uuid', 'ansi-escape-sequences', 'react-email', + '@react-email/components', + '@react-email/render', 'react', 'cdk-nag', 'axios', 'tsx', 'source-map-support', 'cheerio', + 'figlet', + 'prompt-sync', + 'esbuild', + '@graphql-codegen/plugin-helpers', + '@graphql-codegen/cli', + 'typescript', ], devDeps: [ + 'vitepress', + 'aws-lambda', + '@types/ansi-escape-sequences', '@types/prompt-sync', + '@types/figlet', '@types/uuid', 'git-cz', 'cz-conventional-changelog', 'figlet', 'prettier', '@types/cheerio', - ] + ], + tsconfig: { + compilerOptions: { + outDir: 'out', + rootDir: '.', + lib: ['DOM', 'DOM.Iterable', 'ESNext'], + jsx: TypeScriptJsxMode.REACT_JSX, + noEmit: true, + }, + exclude: ['node_modules', 'lib/user-interface/genai-newsletter-ui/*'], + }, }); + +// Existing tasks project.tasks.addTask('config', { description: 'Run the CLI to configure the project', - exec: 'tsx ./cli/config.ts' -}) + exec: 'ts-node ./cli/config.ts', +}); + +const codegenApi = project.tasks.addTask('codegen:api', { + cwd: 'lib/shared/api', + description: 'Generate the API code', + exec: 'npx @aws-amplify/cli codegen', +}); + +const codegenAuth = project.tasks.addTask('codegen:auth', { + cwd: 'lib/shared/api/schema-to-avp', + description: 'Generate the Auth code', + exec: 'npx graphql-codegen ./codegen.ts', +}); + +const codegenTask = project.tasks.addTask('codegen', { + description: 'Run the codegen tasks', +}); + +codegenTask.spawn(codegenApi); +codegenTask.spawn(codegenAuth); -const frontend = new web.ReactTypeScriptProject({ + +project.tasks.addTask('email:start', { + description: 'Start the email generator', + cwd: 'lib/shared/email-generator', + exec: 'npm run start', +}); + +// Updated frontend project with Vite configuration +const frontend = new typescript.TypeScriptProject({ parent: project, outdir: 'lib/user-interface/genai-newsletter-ui/', name: 'genai-newsletter-ui', defaultReleaseBranch: 'main', packageManager: NodePackageManager.NPM, sampleCode: false, + jestOptions: { + jestVersion: '29', + }, deps: [ 'react', 'react-dom', 'react-router-dom', 'graphql', 'react-router', - 'react', - "@aws-amplify/ui-react", + 'aws-amplify', + '@aws-amplify/ui-react', '@cloudscape-design/chat-components', '@cloudscape-design/collection-hooks', '@cloudscape-design/component-toolkit', '@cloudscape-design/global-styles', + ], + devDeps: [ + '@types/react', + '@types/react-dom', + '@vitejs/plugin-react', 'vite', - ] -}) + 'typescript', + 'ts-jest@^29.2.5', + 'eslint-plugin-react-hooks@latest', + 'eslint-plugin-react', + 'eslint-plugin-react-refresh', + ], + tsconfig: { + compilerOptions: { + paths: { + shared: ['../../../shared'], + }, + rootDir: '../../../', + sourceRoot: 'src/', + lib: ['DOM', 'DOM.Iterable', 'ESNext'], + jsx: TypeScriptJsxMode.REACT_JSX, + noEmit: true, + module: 'ESNext', + resolveJsonModule: true, + moduleResolution: TypeScriptModuleResolution.NODE, + skipLibCheck: true, + }, + exclude: ['node_modules/**/*', '../../../node_modules/**/*'], + }, + +}); + +// Update UI tasks to use Vite +frontend.tasks.addTask('dev', { + description: 'Start the UI in development mode', + exec: 'vite', +}); + +frontend.tasks.addTask('preview', { + description: 'Preview the UI build', + exec: 'vite preview', +}); + +frontend.tasks.addTask('start', { + description: 'Start the UI', + exec: 'vite', +}); + +project.tasks.addTask('ui:start', { + description: 'Start the UI', + exec: 'vite', + cwd: 'lib/user-interface/genai-newsletter-ui/', +}); + +const buildAppsync = project.tasks.addTask('build:appsync', { + description: 'Build the appsync', + exec: 'tsx ./lib/api/functions/bundle.ts', +}); + +project.tasks.addTask('docs:dev', { + description: 'Run the docs in dev mode', + exec: 'vitepress dev pages', +}); + +project.tasks.addTask('docs:build', { + description: 'Build the docs', + exec: 'vitepress build pages', +}); + +project.tasks.addTask('docs:preview', { + description: 'Preview the docs', + exec: 'vitepress preview pages', +}); + +project.preCompileTask.spawn(buildAppsync); +project.preCompileTask.spawn(frontend.compileTask); + +// Formatting tasks +project.tasks.addTask('format', { + description: 'Format the code', + exec: 'npm run prettier && npm run lint', +}); + +project.tasks.addTask('lint', { + description: 'Lint the code', + exec: 'eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif --fix', +}); + +project.tasks.addTask('prettier', { + description: 'Run prettier', + exec: 'prettier --write \"**/*.{ts,tsx}\"', +}); + +//Commit friendly messages! +project.tasks.addTask('commit', { + description: 'Commit the code', + exec: 'git-cz', +}); +frontend.synth(); project.synth(); -frontend.synth(); \ No newline at end of file diff --git a/bin/genai-newsletter-app.ts b/bin/genai-newsletter-app.ts index 9e229bd..70ab565 100644 --- a/bin/genai-newsletter-app.ts +++ b/bin/genai-newsletter-app.ts @@ -1,24 +1,20 @@ #!/usr/bin/env node -import 'source-map-support/register' -import { App, Aspects } from 'aws-cdk-lib' -import { GenAINewsletter } from '../lib' -import getConfig from '../lib/config' -import path from 'path' -import { AwsSolutionsChecks } from 'cdk-nag' -import { addNagSuppressions } from '../lib/cdk-nag-supressions' -import { fileURLToPath } from 'url' -import { dirname } from 'path' +import 'source-map-support/register'; +import path from 'path'; +import { App, Aspects } from 'aws-cdk-lib'; +import { AwsSolutionsChecks } from 'cdk-nag'; +import { GenAINewsletter } from '../lib'; +import { addNagSuppressions } from '../lib/cdk-nag-supressions'; +import getConfig from '../lib/config'; -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) -const app = new App() +const app = new App(); -const config = getConfig(path.join(__dirname, 'config.json')) -const baseName = config.stackName ?? 'GenAINewsletter' +const config = getConfig(path.join(__dirname, 'config.json')); +const baseName = config.stackName ?? 'GenAINewsletter'; -Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })) +Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); -const genAiNewsletterApp = new GenAINewsletter(app, baseName) +const genAiNewsletterApp = new GenAINewsletter(app, baseName); -addNagSuppressions(genAiNewsletterApp) +addNagSuppressions(genAiNewsletterApp); diff --git a/cdk.json b/cdk.json index 8079f2f..f61b213 100644 --- a/cdk.json +++ b/cdk.json @@ -1,10 +1,10 @@ { - "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/main.ts", + "app": "npx ts-node -P tsconfig.json --prefer-ts-exts bin/genai-newsletter-app.ts", "output": "cdk.out", "build": "npx projen bundle", "watch": { "include": [ - "src/**/*.ts", + "./**/*.ts", "test/**/*.ts" ], "exclude": [ diff --git a/cli/config-manage.ts b/cli/config-manage.ts index e2c707a..4abfeba 100644 --- a/cli/config-manage.ts +++ b/cli/config-manage.ts @@ -1,436 +1,305 @@ -import * as fs from 'fs' -import { type DeployConfig } from '../lib/shared/common/deploy-config' -import { formatText } from './consts' -import prompt from 'prompt-sync' -import { CONFIG_VERSION } from './config-version' -import path, { dirname } from 'path' -import figlet from 'figlet' -import { fileURLToPath } from 'url' -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const deployConfig = path.join(__dirname, '../bin/config.json') - -const prompter = prompt({ sigint: true }) - -let configStyle: 'EXISTING' | 'UPDATE' | 'NEW' = 'NEW' -let config: DeployConfig | null = null -if (fs.existsSync(deployConfig)) { - const configFromFile: DeployConfig = JSON.parse( - fs.readFileSync(deployConfig, 'utf8') - ) - console.log( - formatText('A configuration already exists.', { - bold: true, - backgroundColor: 'bg-yellow' - }) - ) - let existingConfigChoice: string | null = null - let readyToProceed = false - while (!readyToProceed) { - console.log(formatText('\nDo you want to', { bold: true })) - console.log( - ' ▶️ (' + - formatText('e', { textColor: 'green' }) + - ') Use ' + - formatText('EXISTING', { textColor: 'green' }) + - ' configuration? \n' + - ' ▶️ (' + - formatText('u', { textColor: 'yellow' }) + - ') ' + - formatText('UPDATE', { textColor: 'yellow' }) + - ' existing configuration? \n' + - ' ▶️ (' + - formatText('n', { textColor: 'red' }) + - ') Create a ' + - formatText('NEW', { textColor: 'red' }) + - ' configuration that will replace the existing configuration?' - ) - existingConfigChoice = prompter(formatText('(E/u/n):', { bold: true }), 'e') - if (existingConfigChoice.length > 0) { - existingConfigChoice = existingConfigChoice.toLowerCase() +// config-manage.ts +import * as fs from 'fs'; +import path from 'path'; +import figlet from 'figlet'; +import prompt from 'prompt-sync'; +import { CONFIG_VERSION } from './config-version'; +import { formatText } from './consts'; +import { type DeployConfig } from '../lib/shared/common/deploy-config'; + +const deployConfig = path.join(__dirname, '../bin/config.json'); +const prompter = prompt({ sigint: true }); + +interface ValidatorResponse { + isValid: boolean; + message?: string; +} + +const validators = { + stackName: (value: string): ValidatorResponse => ({ + isValid: value.length > 0, + message: 'Stack name cannot be empty', + }), + + pinpointIdentity: (value: string): ValidatorResponse => { + const arnRegex = /^arn:aws:(ses|pinpoint):[a-z0-9-]+:\d{12}:/; + return { + isValid: arnRegex.test(value), + message: 'Invalid ARN format. Should be a valid SES/Pinpoint ARN', + }; + }, + + email: (value: string): ValidatorResponse => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return { + isValid: emailRegex.test(value), + message: 'Invalid email format', + }; + }, + + hostname: (value: string): ValidatorResponse => { + const hostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; + return { + isValid: hostnameRegex.test(value), + message: 'Invalid hostname format', + }; + }, + + acmCert: (value: string): ValidatorResponse => { + const acmCertRegex = /^arn:aws:acm:\S+:\d+:\w+\/\S+$/; + return { + isValid: acmCertRegex.test(value), + message: 'Invalid ACM certificate ARN format', + }; + }, +}; + +async function promptForValue( + promptValue: string, + currentValue: string | undefined, + validator: (value: string) => ValidatorResponse, + isOptional = false, +): Promise { + let isValid = false; + let value = currentValue; + + while (!isValid) { + const displayPrompt = formatText(promptValue, { textColor: 'blue', bold: true }); + const currentValueDisplay = currentValue + ? formatText(` (current: ${currentValue})`, { textColor: 'gray', italic: true }) + : ''; + + console.log(`\n${displayPrompt}${currentValueDisplay}`); + if (isOptional) { + console.log(formatText('Press Enter to skip', { textColor: 'gray', italic: true })); + } + + const input = prompter('> '); + + if (input === '' && currentValue) { + return currentValue; + } + + if (input === '' && isOptional) { + return undefined; } - switch (existingConfigChoice) { - case 'u': - console.log( - formatText('Update existing configuration', { - bold: true - }) - ) - configStyle = 'UPDATE' - config = configFromFile - readyToProceed = true - break - case 'n': - console.log(formatText('Create a new configuration', { bold: true })) - configStyle = 'NEW' - readyToProceed = true - break - case 'e': - console.log(formatText('Use existing configuration', { bold: true })) - configStyle = 'EXISTING' - config = configFromFile - readyToProceed = true - break - default: - console.log( - formatText('Invalid Input!', { - bold: true, - backgroundColor: 'bg-red', - textColor: 'white' - }) - ) - break + const validation = validator(input); + if (validation.isValid) { + value = input; + isValid = true; + } else { + console.log(formatText(`❌ ${validation.message}`, { + textColor: 'red', + bold: true, + })); } } + + return value; +} + +async function confirmAction(message: string, defaultValue = false): Promise { + const response = prompter( + formatText(`${message} (${defaultValue ? 'Y/n' : 'y/N'}): `, { bold: true }), + defaultValue ? 'Y' : 'N', + ); + return response.toLowerCase() === 'y'; } -console.log( - figlet.textSync('Requirements Check', { - font: 'Mini' - }) -) -console.log( - 'This app relies on certain resources to exist in your AWS environment prior to deployment.' -) -console.log( - 'Please ensure that the following resources exist in your AWS environment before proceeding:\n' -) -console.log( - ' ▶️ SES/Pinpoint Verified Identity ARN for outbound email sending' -) -const requirementsMet = prompter( - formatText('Are you ready to proceed? (Y/n):', { bold: true }), - 'Y' -) -if (requirementsMet.toLowerCase() !== 'y') { + +export async function interactiveManage(): Promise { console.log( - formatText('Exiting...', { + figlet.textSync('Configuration Manager', { + font: 'Mini', + }), + ); + + let config: DeployConfig | null = null; + let configStyle: 'EXISTING' | 'UPDATE' | 'NEW' = 'NEW'; + + // Check for existing configuration + if (fs.existsSync(deployConfig)) { + try { + const configFromFile: DeployConfig = JSON.parse( + fs.readFileSync(deployConfig, 'utf8'), + ); + + console.log( + formatText('\n📁 Existing configuration detected!', { + bold: true, + backgroundColor: 'bg-blue', + textColor: 'white', + }), + ); + + console.log(formatText('\nChoose an option:', { bold: true })); + console.log( + ` ▶️ (${formatText('e', { textColor: 'green' })}) Use ${formatText('EXISTING', { textColor: 'green' })} configuration` + + `\n ▶️ (${formatText('u', { textColor: 'yellow' })}) ${formatText('UPDATE', { textColor: 'yellow' })} existing configuration` + + `\n ▶️ (${formatText('n', { textColor: 'red' })}) Create ${formatText('NEW', { textColor: 'red' })} configuration`, + ); + + let readyToProceed = false; + while (!readyToProceed) { + const choice = prompter(formatText('(E/u/n):', { bold: true }), 'e'); + + switch (choice.toLowerCase()) { + case 'e': + console.log(formatText('\n✅ Using existing configuration', { + textColor: 'green', + bold: true, + })); + configStyle = 'EXISTING'; + config = configFromFile; + readyToProceed = true; + break; + case 'u': + console.log(formatText('\n📝 Updating existing configuration', { + textColor: 'yellow', + bold: true, + })); + configStyle = 'UPDATE'; + config = configFromFile; + readyToProceed = true; + break; + case 'n': + console.log(formatText('\n🆕 Creating new configuration', { + textColor: 'blue', + bold: true, + })); + configStyle = 'NEW'; + readyToProceed = true; + break; + default: + console.log(formatText('❌ Invalid choice!', { + textColor: 'red', + bold: true, + })); + } + } + } catch (error) { + console.log(formatText('\n❌ Error reading existing configuration!', { + backgroundColor: 'bg-red', + textColor: 'white', + bold: true, + })); + configStyle = 'NEW'; + } + } + + // Requirements check + console.log(formatText('\n🔍 Requirements Check', { bold: true })); + console.log(formatText( + 'This app requires certain AWS resources to exist before deployment:', + { italic: true }, + )); + console.log(formatText(' • SES/Pinpoint Verified Identity for email sending', { textColor: 'blue' })); + + const readyToProceed = await confirmAction('\nAre you ready to proceed?', true); + if (!readyToProceed) { + console.log(formatText('\n👋 Configuration cancelled', { + backgroundColor: 'bg-yellow', + textColor: 'black', bold: true, - backgroundColor: 'bg-red', - textColor: 'white' - }) - ) - process.exit(0) -} -if (['UPDATE', 'NEW'].includes(configStyle)) { + })); + return; + } + + // Initialize new configuration if needed if (configStyle === 'NEW') { config = { stackName: 'GenAINewsletter', pinpointEmail: { senderAddress: '', - verifiedIdentity: '' + verifiedIdentity: '', }, configVersion: CONFIG_VERSION, - selfSignUpEnabled: false - } + selfSignUpEnabled: false, + }; } - if (config !== null) { - console.log( - formatText('Updating existing configuration....', { bold: true }) - ) - console.log( - formatText( - 'If a property already exists, it will be shown in parenthesis (). Leave the response blank to keep the existing configuration value\n', - { bold: true } - ) - ) - let stackNameApproved = false - while (!stackNameApproved) { - console.log(formatText('Stack Name:', { textColor: 'blue' })) - const stackName = prompter( - `(${config?.stackName}):`, - config?.stackName ?? '' - ) - if (stackName.length > 0) { - config.stackName = stackName - stackNameApproved = true - } else if ( - config.stackName === undefined || - (config.stackName.length < 1 && stackName.length < 1) - ) { - console.log( - formatText('Invalid Input!', { - backgroundColor: 'bg-white', - textColor: 'red', - bold: true - }) - ) - } else if (config.stackName.length > 0) { - stackNameApproved = true - } - } - let pinpointIdentityApproved = false - while (!pinpointIdentityApproved) { - console.log( - formatText('SES/Pinpoint Verified Identity ARN:', { - textColor: 'blue' - }) - ) - const pinpointIdentity = prompter( - `(${config?.pinpointEmail.verifiedIdentity}):`, - config?.pinpointEmail.verifiedIdentity ?? '' - ) - if (pinpointIdentity.length > 0) { - config.pinpointEmail.verifiedIdentity = pinpointIdentity - pinpointIdentityApproved = true - } else if ( - config.pinpointEmail.verifiedIdentity.length < 1 && - pinpointIdentity.length < 1 - ) { - console.log( - formatText('Invalid Input!', { - backgroundColor: 'bg-white', - textColor: 'red', - bold: true - }) - ) - } else if (config.pinpointEmail.verifiedIdentity.length > 0) { - pinpointIdentityApproved = true - } - } - let pinpointSenderApproved = false - while (!pinpointSenderApproved) { - console.log( - formatText( - 'What is the email address used to send Newsletter emails?', - { textColor: 'blue' } - ) - ) - console.log( - formatText( - 'Note: this email should be part of the approved identity you provided', - { italic: true } - ) - ) - const pinpointSender = prompter( - `(${config?.pinpointEmail.senderAddress}):`, - config?.pinpointEmail.senderAddress ?? '' - ) - if (pinpointSender.length > 0) { - config.pinpointEmail.senderAddress = pinpointSender - pinpointSenderApproved = true - break - } - console.log( - formatText('Invalid Input!', { - backgroundColor: 'bg-white', - textColor: 'red', - bold: true - }) - ) - } - let addEnvData = false - if (config.env === undefined) { - console.log( - formatText( - 'Do you want to set the deployment AWS Account ID & Region?', - { textColor: 'blue' } - ) - ) - console.log( - formatText( - 'Not typically needed, but useful if you want to persist deployment destintation outside of the CDK context', - { italic: true } - ) - ) - const addEnv = prompter( - formatText('Do you want to proceed? (y/N):', { bold: true }), - 'N' - ) - if (addEnv.toLowerCase() === 'y') { - addEnvData = true + + if (config && ['UPDATE', 'NEW'].includes(configStyle)) { + console.log(formatText('\n📝 Configuration Setup', { bold: true })); + + // Stack Configuration + console.log(formatText('\n🏗️ Stack Configuration', { textColor: 'blue', bold: true })); + config.stackName = await promptForValue( + 'Stack Name:', + config.stackName, + validators.stackName, + ) || 'GenAINewsletter'; + + // Email Configuration + console.log(formatText('\n📧 Email Configuration', { textColor: 'blue', bold: true })); + config.pinpointEmail.verifiedIdentity = await promptForValue( + 'SES/Pinpoint Verified Identity ARN:', + config.pinpointEmail.verifiedIdentity, + validators.pinpointIdentity, + ) || ''; + + config.pinpointEmail.senderAddress = await promptForValue( + 'Sender Email Address:', + config.pinpointEmail.senderAddress, + validators.email, + ) || ''; + + // Authentication Configuration + console.log(formatText('\n🔐 Authentication Configuration', { textColor: 'blue', bold: true })); + config.selfSignUpEnabled = await confirmAction('Enable self sign-up?', false); + + // Optional UI Configuration + if (await confirmAction('\nDo you want to configure a custom frontend hostname?', false)) { + if (!config.ui) config.ui = {}; + + config.ui.hostName = await promptForValue( + 'Frontend Hostname:', + config.ui?.hostName, + validators.hostname, + true, + ); + + if (config.ui.hostName) { + config.ui.acmCertificateArn = await promptForValue( + 'ACM Certificate ARN:', + config.ui?.acmCertificateArn, + validators.acmCert, + ); } } - if ( - addEnvData || - config.env?.account != null || - config.env?.region != null - ) { - console.log( - formatText('Deployment AWS Account ID', { textColor: 'blue' }) - ) + + // Optional Environment Configuration + if (await confirmAction('\nDo you want to set deployment account and region?', false)) { + if (!config.env) config.env = {}; + const accountId = prompter( - config?.env?.account !== undefined - ? `(${config?.env?.account}):` - : 'Account ID:', - config?.env?.account ?? '' - ) - if (config.env == null) { - config.env = {} - } - config.env.account = accountId ?? '' - console.log(formatText('Deployment AWS Region', { textColor: 'blue' })) + formatText('\nAWS Account ID:', { textColor: 'blue', bold: true }) + + (config.env.account ? formatText(` (current: ${config.env.account})`, { textColor: 'gray', italic: true }) : '') + + '\n> ', + ); + if (accountId) config.env.account = accountId; + const region = prompter( - config?.env?.region !== undefined - ? `(${config?.env?.region}):` - : 'AWS Region:', - config?.env?.region ?? '' - ) - config.env.region = region ?? '' - } - let selfSignUpResponseApproved = false - while (!selfSignUpResponseApproved) { - const response = prompter( - formatText('Do you want to enable self sign up? (y/N):', { - bold: true, - textColor: 'blue' - }), - 'N' - ) - if (response.toLowerCase() === 'y') { - config.selfSignUpEnabled = true - selfSignUpResponseApproved = true - } else if (response.toLowerCase() === 'n') { - selfSignUpResponseApproved = true - config.selfSignUpEnabled = false - } else { - console.log( - formatText('Invalid Input!', { - backgroundColor: 'bg-white', - textColor: 'red', - bold: true - }) - ) - } - } - /** - * Does the user want to configure a Host Name & ACM Cert for the Frontend Cloudfront - */ - let configHostname = false - if ( - config.ui?.acmCertificateArn === undefined || - config.ui.hostName === undefined - ) { - console.log( - formatText( - 'Do you want to configure a custom hostname for the frontend?', - { textColor: 'blue' } - ) - ) - console.log( - formatText( - 'Requires a hostname & a pre-existing AWS Certificate Manager public cert ARN.' + - 'For more information, visit https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html', - { italic: true } - ) - ) - let loopA = true - while (loopA) { - const response = prompter( - formatText('Do you want to proceed? (y/N):', { - bold: true, - textColor: 'blue' - }), - 'N' - ) - if (response.toLowerCase() === 'y') { - configHostname = true - loopA = false - break - } else if (response.toLowerCase() === 'n') { - loopA = false - break - } else { - console.log( - formatText('Invalid Input!', { - backgroundColor: 'bg-white', - textColor: 'red', - bold: true - }) - ) - } - } - if (configHostname) { - let loopB = true - while (loopB) { - const existingHostname = - config.ui?.hostName != null ? config.ui.hostName : '' - const response = prompter( - formatText( - `Enter the hostname you want to use for the frontend:(${existingHostname})`, - { textColor: 'blue' } - ) - ) - const hostnameRegex = - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ - if (response.length > 0 && hostnameRegex.test(response)) { - if (config.ui === undefined) { - config.ui = {} - } - config.ui.hostName = response - loopB = false - break - } else if ( - response.length < 1 && - config.ui?.hostName !== undefined && - config.ui.hostName !== null - ) { - loopB = false - break - } else { - console.log( - formatText('Invalid Input!', { - backgroundColor: 'bg-white', - textColor: 'red', - bold: true - }) - ) - } - } - let loopC = true - while (loopC) { - const existingAcmCert = - config.ui?.acmCertificateArn != null - ? config.ui.acmCertificateArn - : '' - const response = prompter( - formatText( - `Enter the ACM Certificate ARN you want to use for the frontend:(${existingAcmCert})`, - { textColor: 'blue' } - ) - ) - const acmCertRegex = /^arn:aws:acm:\S+:\d+:\w+\/\S+$/ - if (response.length > 0 && acmCertRegex.test(response)) { - if (config.ui === undefined) { - config.ui = {} - } - config.ui.acmCertificateArn = response - loopC = false - break - } else if ( - response.length < 1 && - config.ui?.acmCertificateArn !== undefined && - config.ui.acmCertificateArn !== null - ) { - loopC = false - break - } else { - console.log( - formatText('Invalid Input!', { - backgroundColor: 'bg-white', - textColor: 'red', - bold: true - }) - ) - } - } - } + formatText('\nAWS Region:', { textColor: 'blue', bold: true }) + + (config.env.region ? formatText(` (current: ${config.env.region})`, { textColor: 'gray', italic: true }) : '') + + '\n> ', + ); + if (region) config.env.region = region; } - } - fs.writeFileSync(deployConfig, JSON.stringify(config, null, '\t')) -} -if (config !== null) { - console.log( - formatText('Configuration Complete! 💫', { + + // Save configuration + fs.writeFileSync(deployConfig, JSON.stringify(config, null, 2)); + + console.log(formatText('\n✅ Configuration saved successfully!', { backgroundColor: 'bg-green', textColor: 'white', - bold: true - }) - ) - console.log( - formatText('You GenAI Newsletter App Stack is ready for deployment. 🥳', { - bold: true - }) - ) -} + bold: true, + })); + } + + if (config) { + console.log(formatText('\n🚀 Your GenAI Newsletter App Stack is ready for deployment!', { + bold: true, + textColor: 'green', + })); + } +} \ No newline at end of file diff --git a/cli/config-show.ts b/cli/config-show.ts index b60404d..1c9289a 100644 --- a/cli/config-show.ts +++ b/cli/config-show.ts @@ -1,18 +1,150 @@ -import * as fs from 'fs' -import { type DeployConfig } from '../lib/shared/common/deploy-config' -import { bigHeader } from './consts' -const configFile = './bin/config.json' - -console.log('Checking for existing configuration....') -if (fs.existsSync(configFile)) { - const config: DeployConfig = JSON.parse(fs.readFileSync(configFile, 'utf8')) - console.log('Deployment Configuration Located!') - console.log(bigHeader('GenAI Newsletter Deployment Configuration')) - console.log(JSON.stringify(config, null, '\t')) -} else { - console.log('No config file found in ./bin/config.json!') - console.log('#######################') - console.log( - 'Please run "npm run config" to setup your deployment configurations.' - ) +// config-show.ts +import * as fs from 'fs'; +import { CONFIG_VERSION } from './config-version'; +import { bigHeader, formatText } from './consts'; +import { type DeployConfig } from '../lib/shared/common/deploy-config'; +const configFile = './bin/config.json'; + +function formatJsonValue(value: any, indent: number = 0): string { + if (value === null) return formatText('null', { textColor: 'gray' }); + if (value === undefined) return formatText('undefined', { textColor: 'gray' }); + + switch (typeof value) { + case 'string': + return formatText(`"${value}"`, { textColor: 'green' }); + case 'number': + return formatText(value.toString(), { textColor: 'yellow' }); + case 'boolean': + return formatText(value.toString(), { textColor: 'cyan' }); + case 'object': + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + const arrayItems = value + .map(item => formatJsonValue(item, indent + 2)) + .join(',\n' + ' '.repeat(indent + 2)); + return `[\n${' '.repeat(indent + 2)}${arrayItems}\n${' '.repeat(indent)}]`; + } + + const entries = Object.entries(value); + if (entries.length === 0) return '{}'; + + const formattedEntries = entries + .map(([key, val]) => { + const formattedKey = formatText(`"${key}"`, { textColor: 'blue' }); + return `${' '.repeat(indent + 2)}${formattedKey}: ${formatJsonValue(val, indent + 2)}`; + }) + .join(',\n'); + + return `{\n${formattedEntries}\n${' '.repeat(indent)}}`; + default: + return String(value); + } } + +export function showConfig(): void { + console.log(formatText('\nChecking for existing configuration....', { bold: true })); + + if (!fs.existsSync(configFile)) { + console.log( + formatText('\n⚠️ No configuration file found!', { + backgroundColor: 'bg-yellow', + textColor: 'black', + bold: true, + }), + ); + console.log( + formatText('\nPlease select MANAGE from the main menu to setup your deployment configurations.', { + textColor: 'yellow', + italic: true, + }), + ); + return; + } + + try { + const config: DeployConfig = JSON.parse(fs.readFileSync(configFile, 'utf8')); + console.log( + formatText('\n✅ Deployment Configuration Located!', { + backgroundColor: 'bg-green', + textColor: 'white', + bold: true, + }), + ); + + // Version check + if (config.configVersion !== CONFIG_VERSION) { + console.log( + formatText( + `\n⚠️ Configuration version mismatch! Current: ${config.configVersion}, Latest: ${CONFIG_VERSION}`, { + backgroundColor: 'bg-yellow', + textColor: 'black', + bold: true, + }), + ); + } + + // Configuration Details Section + console.log(bigHeader('Configuration Details')); + + // Stack Info + console.log(formatText('📚 Stack Information', { textColor: 'blue', bold: true })); + console.log(formatText('├─ Stack Name:', { textColor: 'white' }), formatText(config.stackName ?? 'GenAI Newsletter App', { textColor: 'green' })); + + // Email Configuration + console.log(formatText('\n📧 Email Configuration', { textColor: 'blue', bold: true })); + console.log(formatText('├─ Sender Address:', { textColor: 'white' }), formatText(config.pinpointEmail.senderAddress, { textColor: 'green' })); + console.log(formatText('└─ Verified Identity:', { textColor: 'white' }), formatText(config.pinpointEmail.verifiedIdentity, { textColor: 'green' })); + + // Auth Configuration + console.log(formatText('\n🔐 Authentication', { textColor: 'blue', bold: true })); + console.log( + formatText('└─ Self Sign-up:', { textColor: 'white' }), + config.selfSignUpEnabled + ? formatText('Enabled', { textColor: 'green' }) + : formatText('Disabled', { textColor: 'red' }), + ); + + // UI Configuration + if (config.ui) { + console.log(formatText('\n🖥️ UI Configuration', { textColor: 'blue', bold: true })); + if (config.ui.hostName) { + console.log(formatText('├─ Hostname:', { textColor: 'white' }), formatText(config.ui.hostName, { textColor: 'green' })); + } + if (config.ui.acmCertificateArn) { + console.log(formatText('└─ ACM Certificate:', { textColor: 'white' }), formatText(config.ui.acmCertificateArn, { textColor: 'green' })); + } + } + + // Environment Configuration + if (config.env) { + console.log(formatText('\n🌍 Environment', { textColor: 'blue', bold: true })); + if (config.env.account) { + console.log(formatText('├─ AWS Account:', { textColor: 'white' }), formatText(config.env.account, { textColor: 'green' })); + } + if (config.env.region) { + console.log(formatText('└─ AWS Region:', { textColor: 'white' }), formatText(config.env.region, { textColor: 'green' })); + } + } + + // Raw JSON output + console.log(formatText('\n📋 Raw Configuration', { textColor: 'blue', bold: true })); + console.log(formatJsonValue(config)); + + } catch (error) { + console.log( + formatText('\n❌ Error reading configuration file!', { + backgroundColor: 'bg-red', + textColor: 'white', + bold: true, + }), + ); + if (error instanceof Error) { + console.log( + formatText(error.message, { + textColor: 'red', + italic: true, + }), + ); + } + } +} \ No newline at end of file diff --git a/cli/config-version.ts b/cli/config-version.ts index 2dcd1eb..3425bff 100644 --- a/cli/config-version.ts +++ b/cli/config-version.ts @@ -1,2 +1,2 @@ -export const CONFIG_VERSION = '0.0.4' -export const MIN_VERSION = '0.0.2' +export const CONFIG_VERSION = '0.0.4'; +export const MIN_VERSION = '0.0.2'; diff --git a/cli/config.ts b/cli/config.ts index 5f8e953..0876a93 100644 --- a/cli/config.ts +++ b/cli/config.ts @@ -1,38 +1,63 @@ -import { Command } from 'commander' -import { CONFIG_VERSION } from './config-version' -import figlet from 'figlet' +// config.ts +import figlet from 'figlet'; +import prompt from 'prompt-sync'; +import { interactiveManage } from './config-manage'; +import { showConfig } from './config-show'; +import { formatText } from './consts'; -const program = new Command() +const prompter = prompt({ sigint: true }); console.log( figlet.textSync('GenAI Newsletter', { - font: 'Slant' - }) -) + font: 'Slant', + }), +); -program - .name('npm run config') - .description( - 'CLI utility for creating, viewing, and updating you GenAI Newsletter deployment configuration.' - ) - .version(CONFIG_VERSION) - .configureOutput({ - writeOut: (str) => { - try { - const config = JSON.parse(str) - console.log(JSON.stringify(config, null, '\t')) - } catch (e) { - console.log(str) - } - } - }) +// Main interactive menu +async function mainMenu(): Promise { + let exit = false; + while (!exit) { + console.log(formatText('\nWhat would you like to do?', { bold: true })); + console.log( + ' ▶️ (' + + formatText('m', { textColor: 'green' }) + + ') ' + + formatText('MANAGE', { textColor: 'green' }) + + ' configuration\n' + + ' ▶️ (' + + formatText('s', { textColor: 'blue' }) + + ') ' + + formatText('SHOW', { textColor: 'blue' }) + + ' current configuration\n' + + ' ▶️ (' + + formatText('x', { textColor: 'red' }) + + ') ' + + formatText('EXIT', { textColor: 'red' }), + ); -program - .command('show', '🖨️ Show the current deployment configuration details') - .description('Show the current deployment configuration details') + const choice = prompter(formatText('(M/s/x):', { bold: true }), 'm'); -program - .command('manage', '📝 Create or Manage the deployment configuration') - .description('Create a new deployment configuration') + switch (choice.toLowerCase()) { + case 'm': + await interactiveManage(); + break; + case 's': + showConfig(); + break; + case 'x': + exit = true; + break; + default: + console.log( + formatText('Invalid Input!', { + bold: true, + backgroundColor: 'bg-red', + textColor: 'white', + }), + ); + } + } +} -program.parse() +// Start the CLI +mainMenu().catch(console.error); \ No newline at end of file diff --git a/cli/consts.ts b/cli/consts.ts index adf3c84..73dc099 100644 --- a/cli/consts.ts +++ b/cli/consts.ts @@ -1,50 +1,50 @@ -import ansi from 'ansi-escape-sequences' +import ansi from 'ansi-escape-sequences'; interface FormattingOptions { - bigHeader?: boolean - bold?: boolean - italic?: boolean - underline?: boolean - textColor?: ansi.Style - backgroundColor?: ansi.Style + bigHeader?: boolean; + bold?: boolean; + italic?: boolean; + underline?: boolean; + textColor?: ansi.Style; + backgroundColor?: ansi.Style; } export const bigHeader = (text: string): string => { - const maxLineLength = 80 - const maxTextLength = maxLineLength - 10 - const lines = [] - const endLine = '#'.repeat(maxLineLength) - const blankLine = '#' + ' '.repeat(maxLineLength - 2) + '#' + const maxLineLength = 80; + const maxTextLength = maxLineLength - 10; + const lines = []; + const endLine = '#'.repeat(maxLineLength); + const blankLine = '#' + ' '.repeat(maxLineLength - 2) + '#'; const generateSpacedLine = (subtext: string, length: number): string => { return ( '#' + ' '.repeat( - (length - subtext.length) / 2 - ((length - subtext.length) % 2) + (length - subtext.length) / 2 - ((length - subtext.length) % 2), ) + subtext + ' '.repeat( - (length - subtext.length) / 2 - ((length - subtext.length) % 2) + (length - subtext.length) / 2 - ((length - subtext.length) % 2), ) + '#' - ) - } - lines.push(endLine) - lines.push(blankLine) + ); + }; + lines.push(endLine); + lines.push(blankLine); if (text.length > maxTextLength) { - let remainingText = text + let remainingText = text; while (remainingText.length > 0) { - let subtext = remainingText.substring(0, maxTextLength) - subtext = subtext.substring(0, subtext.lastIndexOf(' ')) - lines.push(generateSpacedLine(subtext, maxLineLength)) - remainingText = remainingText.substring(subtext.length) + let subtext = remainingText.substring(0, maxTextLength); + subtext = subtext.substring(0, subtext.lastIndexOf(' ')); + lines.push(generateSpacedLine(subtext, maxLineLength)); + remainingText = remainingText.substring(subtext.length); } } else { - lines.push(generateSpacedLine(text, maxLineLength)) + lines.push(generateSpacedLine(text, maxLineLength)); } - lines.push(blankLine) - lines.push(endLine) - return lines.join('\n') + '\n' -} + lines.push(blankLine); + lines.push(endLine); + return lines.join('\n') + '\n'; +}; /** * Used to format text for the CLI. Should be applied to plain strings. @@ -54,30 +54,30 @@ export const bigHeader = (text: string): string => { */ export const formatText = ( text: string, - formatting: FormattingOptions + formatting: FormattingOptions, ): string => { - let outputText = '' - const styles: ansi.Style[] = [] + let outputText = ''; + const styles: ansi.Style[] = []; if (formatting.bold === true) { - styles.push('bold') + styles.push('bold'); } if (formatting.textColor !== undefined) { - styles.push(formatting.textColor) + styles.push(formatting.textColor); } if (formatting.backgroundColor !== undefined) { - styles.push(formatting.backgroundColor) + styles.push(formatting.backgroundColor); } if (formatting.italic === true) { - styles.push('italic') + styles.push('italic'); } if (formatting.underline === true) { - styles.push('underline') + styles.push('underline'); } if (styles.length > 0) { - outputText = ansi.format(text, styles) + outputText = ansi.format(text, styles); } if (formatting.bigHeader === true) { - outputText = '\n' + bigHeader(outputText) + '\n' + outputText = '\n' + bigHeader(outputText) + '\n'; } - return outputText -} + return outputText; +}; diff --git a/lib/api/functions/bundle.ts b/lib/api/functions/bundle.ts index ce7db74..2c15804 100644 --- a/lib/api/functions/bundle.ts +++ b/lib/api/functions/bundle.ts @@ -10,28 +10,22 @@ * to files dynamically without using 'globs' since they are not safe for windows */ -import esbuild from 'esbuild' -import fs from 'fs' -import path from 'path' +import fs from 'fs'; +import path from 'path'; +import esbuild from 'esbuild'; -import { fileURLToPath } from 'url' -import { dirname } from 'path' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const outDir = path.join(__dirname, 'out') +const outDir = path.join(__dirname, 'out'); const resolverFunctions = fs .readdirSync(path.join(__dirname, 'resolver')) .map((functionName) => { - return path.join(__dirname, 'resolver', functionName, 'index.ts') - }) + return path.join(__dirname, 'resolver', functionName, 'index.ts'); + }); const pipelineFunctions = fs .readdirSync(path.join(__dirname, 'pipeline')) .map((functionName) => { - return path.join(__dirname, 'pipeline', functionName, 'index.ts') - }) + return path.join(__dirname, 'pipeline', functionName, 'index.ts'); + }); esbuild .build({ @@ -45,6 +39,6 @@ esbuild target: 'esnext', format: 'esm', minify: false, - logLevel: 'info' + logLevel: 'info', }) - .catch(() => process.exit(1)) + .catch(() => process.exit(1)); diff --git a/lib/api/functions/index.ts b/lib/api/functions/index.ts index 8d0bf80..2739598 100644 --- a/lib/api/functions/index.ts +++ b/lib/api/functions/index.ts @@ -4,4 +4,4 @@ * SPDX-License-Identifier: MIT-0 */ -export * from './resolver-helper' +export * from './resolver-helper'; diff --git a/lib/api/functions/pipeline/checkSubscriptionToNewsletter/index.ts b/lib/api/functions/pipeline/checkSubscriptionToNewsletter/index.ts index 29be652..368a931 100644 --- a/lib/api/functions/pipeline/checkSubscriptionToNewsletter/index.ts +++ b/lib/api/functions/pipeline/checkSubscriptionToNewsletter/index.ts @@ -2,28 +2,28 @@ import { type Context, util, type DynamoDBGetItemRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; export function request (ctx: Context): DynamoDBGetItemRequest { - const identity = ctx.identity as AppSyncIdentityLambda - const { id } = ctx.args.input + const identity = ctx.identity as AppSyncIdentityLambda; + const { id } = ctx.args.input; return ddb.get({ key: { newsletterId: id, - sk: 'subscriber#' + identity.resolverContext.userId - } - }) + sk: 'subscriber#' + identity.resolverContext.userId, + }, + }); } export const response = (ctx: Context): any => { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } if (ctx.result !== undefined) { - return true + return true; } else { - return false + return false; } -} +}; diff --git a/lib/api/functions/pipeline/createDataFeed/index.ts b/lib/api/functions/pipeline/createDataFeed/index.ts index fd52d44..4f181b6 100644 --- a/lib/api/functions/pipeline/createDataFeed/index.ts +++ b/lib/api/functions/pipeline/createDataFeed/index.ts @@ -2,32 +2,32 @@ import { type Context, util, type LambdaRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; export function request (ctx: Context): LambdaRequest { - const { args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda - const input = args.input + const { args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; + const input = args.input; if (input.isPrivate === undefined || input.isPrivate === null) { - input.isPrivate = true + input.isPrivate = true; } console.log('payload', { accountId: identity.resolverContext.accountId, - input - }) + input, + }); return { operation: 'Invoke', payload: { accountId: identity.resolverContext.accountId, - input - } - } + input, + }, + }; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/pipeline/createNewsletter/index.ts b/lib/api/functions/pipeline/createNewsletter/index.ts index 6d21a61..ca2ab90 100644 --- a/lib/api/functions/pipeline/createNewsletter/index.ts +++ b/lib/api/functions/pipeline/createNewsletter/index.ts @@ -2,31 +2,31 @@ import { type Context, util, type LambdaRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; export function request (ctx: Context): LambdaRequest { - const { args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda - const input = args.input + const { args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; + const input = args.input; if (input.isPrivate === undefined || input.isPrivate === null) { - input.isPrivate = true + input.isPrivate = true; } return { operation: 'Invoke', payload: { createdBy: { accountId: identity.resolverContext.accountId, - userId: identity.resolverContext.userId + userId: identity.resolverContext.userId, }, - input - } - } + input, + }, + }; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/pipeline/filterListByAuthorization/index.ts b/lib/api/functions/pipeline/filterListByAuthorization/index.ts index 955045f..96abecb 100644 --- a/lib/api/functions/pipeline/filterListByAuthorization/index.ts +++ b/lib/api/functions/pipeline/filterListByAuthorization/index.ts @@ -8,45 +8,45 @@ import { type LambdaRequest, util, type Context, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' -import { convertAvpObjectsToGraphql } from '../../resolver-helper' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; +import { convertAvpObjectsToGraphql } from '../../resolver-helper'; export function request (ctx: Context): LambdaRequest { console.log( - `[Filter List by Authorization Request] request ctx ${JSON.stringify(ctx)}` - ) - const { source, args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda + `[Filter List by Authorization Request] request ctx ${JSON.stringify(ctx)}`, + ); + const { source, args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; return { operation: 'Invoke', payload: { userId: identity.resolverContext.userId, accountId: identity.resolverContext.accountId, requestContext: JSON.parse( - identity.resolverContext.requestContext as string + identity.resolverContext.requestContext as string, ), result: ctx.prev.result, arguments: args, - source - } - } + source, + }, + }; } export function response (ctx: Context): any { - console.log('[IsAuthorized] response ctx $', { ctx: JSON.stringify(ctx) }) - const { error, result } = ctx + console.log('[IsAuthorized] response ctx $', { ctx: JSON.stringify(ctx) }); + const { error, result } = ctx; if (error !== undefined && error !== null) { - util.appendError(error.message, error.type, result) + util.appendError(error.message, error.type, result); } if (result.isAuthorized !== true) { - util.unauthorized() + util.unauthorized(); } console.log('[IsAuthorized] response result $', { - result: JSON.stringify(result) - }) + result: JSON.stringify(result), + }); return { isAuthorized: true, - items: convertAvpObjectsToGraphql(result) - } + items: convertAvpObjectsToGraphql(result), + }; } diff --git a/lib/api/functions/pipeline/flagArticle/index.ts b/lib/api/functions/pipeline/flagArticle/index.ts index 9ef9d2e..12ddf20 100644 --- a/lib/api/functions/pipeline/flagArticle/index.ts +++ b/lib/api/functions/pipeline/flagArticle/index.ts @@ -1,26 +1,26 @@ import { type Context, util, - type DynamoDBUpdateItemRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBUpdateItemRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; export function request (ctx: Context): DynamoDBUpdateItemRequest { - const { dataFeedId, articleId, flaggedContent } = ctx.args.input + const { dataFeedId, articleId, flaggedContent } = ctx.args.input; return ddb.update({ key: { dataFeedId, - sk: 'article#' + articleId + sk: 'article#' + articleId, }, update: { - flaggedContent - } - }) + flaggedContent, + }, + }); } export const response = (ctx: Context): any => { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return true -} + return true; +}; diff --git a/lib/api/functions/pipeline/getDataFeed/index.ts b/lib/api/functions/pipeline/getDataFeed/index.ts index e529ea2..922fd86 100644 --- a/lib/api/functions/pipeline/getDataFeed/index.ts +++ b/lib/api/functions/pipeline/getDataFeed/index.ts @@ -1,36 +1,36 @@ -import { type DynamoDBGetItemRequest, type Context } from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' +import { type DynamoDBGetItemRequest, type Context } from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItem, - convertFieldIdToObjectId -} from '../../resolver-helper' + convertFieldIdToObjectId, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBGetItemRequest { - const { id } = ctx.args.input - console.log(ctx.identity) + const { id } = ctx.args.input; + console.log(ctx.identity); return ddb.get({ key: { dataFeedId: id, - sk: 'dataFeed' - } - }) + sk: 'dataFeed', + }, + }); } export const response = (ctx: Context): any => { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } if (ctx.result === undefined || ctx.result === null) { - util.error('DataFeed not found', 'DataFeedNotFound') + util.error('DataFeed not found', 'DataFeedNotFound'); } if (ctx.result.dataFeedId !== undefined) { if (ctx.result.isPrivate === undefined || ctx.result.isPrivate === null) { - ctx.result.isPrivate = true + ctx.result.isPrivate = true; } } - let result = ctx.result - result = addAccountToItem(result) - result = convertFieldIdToObjectId(result, 'dataFeedId') + let result = ctx.result; + result = addAccountToItem(result); + result = convertFieldIdToObjectId(result, 'dataFeedId'); - return result -} + return result; +}; diff --git a/lib/api/functions/pipeline/getNewsletter/index.ts b/lib/api/functions/pipeline/getNewsletter/index.ts index 93e3974..22a01a0 100644 --- a/lib/api/functions/pipeline/getNewsletter/index.ts +++ b/lib/api/functions/pipeline/getNewsletter/index.ts @@ -1,33 +1,33 @@ import { type Context, util, - type DynamoDBGetItemRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBGetItemRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItem, - convertFieldIdToObjectId -} from '../../resolver-helper' + convertFieldIdToObjectId, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBGetItemRequest { - console.log('getNewsletter request', { ctx }) - const { id } = ctx.args.input + console.log('getNewsletter request', { ctx }); + const { id } = ctx.args.input; return ddb.get({ key: { newsletterId: id, - sk: 'newsletter' - } - }) + sk: 'newsletter', + }, + }); } export const response = (ctx: Context): any => { - console.log('getNewsletter response', { ctx }) + console.log('getNewsletter response', { ctx }); if (ctx.error != null) { - util.appendError(ctx.error.message, ctx.error.type) - return ctx.error.message + util.appendError(ctx.error.message, ctx.error.type); + return ctx.error.message; } - let result = ctx.result - result = addAccountToItem(result) - result = convertFieldIdToObjectId(result, 'newsletterId') - return result -} + let result = ctx.result; + result = addAccountToItem(result); + result = convertFieldIdToObjectId(result, 'newsletterId'); + return result; +}; diff --git a/lib/api/functions/pipeline/getNewsletterSubscriberStats/index.ts b/lib/api/functions/pipeline/getNewsletterSubscriberStats/index.ts index bb79e63..5aebd43 100644 --- a/lib/api/functions/pipeline/getNewsletterSubscriberStats/index.ts +++ b/lib/api/functions/pipeline/getNewsletterSubscriberStats/index.ts @@ -1,24 +1,24 @@ -import * as ddb from '@aws-appsync/utils/dynamodb' import { type Context, type DynamoDBQueryRequest, - util -} from '@aws-appsync/utils' + util, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; export function request (ctx: Context): DynamoDBQueryRequest { - const { id } = ctx.args.input + const { id } = ctx.args.input; return ddb.query({ query: { newsletterId: { eq: id }, - sk: { beginsWith: 'subscriber#' } + sk: { beginsWith: 'subscriber#' }, }, - consistentRead: false - }) + consistentRead: false, + }); } export function response (ctx: Context): any { if (ctx?.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } if ( ctx.result === undefined || @@ -27,12 +27,12 @@ export function response (ctx: Context): any { ) { return { id: ctx.args.input.id, - count: 0 - } + count: 0, + }; } else { return { id: ctx.args.input.id, - count: ctx.result.items.length - } + count: ctx.result.items.length, + }; } } diff --git a/lib/api/functions/pipeline/getPublication/index.ts b/lib/api/functions/pipeline/getPublication/index.ts index 552ac79..cc808e3 100644 --- a/lib/api/functions/pipeline/getPublication/index.ts +++ b/lib/api/functions/pipeline/getPublication/index.ts @@ -1,56 +1,56 @@ import { type Context, util, - type DynamoDBGetItemRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBGetItemRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItem, - convertFieldIdToObjectId -} from '../../resolver-helper' + convertFieldIdToObjectId, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBGetItemRequest { - const { newsletterId, publicationId } = ctx.args.input + const { newsletterId, publicationId } = ctx.args.input; return ddb.get({ key: { newsletterId, - sk: 'publication#' + publicationId - } - }) + sk: 'publication#' + publicationId, + }, + }); } export const response = (ctx: Context): any => { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } const { emailKey, createdAt, newsletterId, publicationId, accountId } = - ctx.result - let path = '' + ctx.result; + let path = ''; if (emailKey !== undefined) { - path = emailKey + path = emailKey; if (path.indexOf('/') !== 0) { - path = '/' + path + path = '/' + path; } } else { const epochCreatedAt = util.time.parseISO8601ToEpochMilliSeconds( - createdAt as string - ) - const year = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'YYYY') - const month = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'MM') - const day = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'DD') - path = `/newsletter-content/${year}/${month}/${day}/${publicationId}` + createdAt as string, + ); + const year = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'YYYY'); + const month = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'MM'); + const day = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'DD'); + path = `/newsletter-content/${year}/${month}/${day}/${publicationId}`; } - const htmlPath = path + '.html' - const textPath = path + '.txt' + const htmlPath = path + '.html'; + const textPath = path + '.txt'; let result = { newsletterId, publicationId, accountId, createdAt, htmlPath, - textPath - } - result = addAccountToItem(result) - result = convertFieldIdToObjectId(result, 'publicationId') - return result -} + textPath, + }; + result = addAccountToItem(result); + result = convertFieldIdToObjectId(result, 'publicationId'); + return result; +}; diff --git a/lib/api/functions/pipeline/isAuthorized/index.ts b/lib/api/functions/pipeline/isAuthorized/index.ts index f72ea40..2ea0694 100644 --- a/lib/api/functions/pipeline/isAuthorized/index.ts +++ b/lib/api/functions/pipeline/isAuthorized/index.ts @@ -8,37 +8,37 @@ import { type LambdaRequest, util, type Context, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' -import { convertAvpObjectToGraphql } from '../../resolver-helper' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; +import { convertAvpObjectToGraphql } from '../../resolver-helper'; export function request (ctx: Context): LambdaRequest { - const { source, args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda + const { source, args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; return { operation: 'Invoke', payload: { userId: identity.resolverContext.userId, accountId: identity.resolverContext.accountId, requestContext: JSON.parse( - identity.resolverContext.requestContext as string + identity.resolverContext.requestContext as string, ), result: ctx.prev.result, arguments: args, source, root: ctx.stash.root, - contingentAction: ctx.stash.contingentAction - } - } + contingentAction: ctx.stash.contingentAction, + }, + }; } export function response (ctx: Context): any { - const { error, result } = ctx + const { error, result } = ctx; if (error !== undefined && error !== null) { - util.error(error.message, error.type, result) + util.error(error.message, error.type, result); } if (result.isAuthorized !== true) { - util.unauthorized() + util.unauthorized(); } - return convertAvpObjectToGraphql(result.returnResult) + return convertAvpObjectToGraphql(result.returnResult); } diff --git a/lib/api/functions/pipeline/listArticles/index.ts b/lib/api/functions/pipeline/listArticles/index.ts index 61c828d..a64adfd 100644 --- a/lib/api/functions/pipeline/listArticles/index.ts +++ b/lib/api/functions/pipeline/listArticles/index.ts @@ -1,42 +1,42 @@ import { type Context, type DynamoDBQueryRequest, - util -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + util, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; export function request (ctx: Context): DynamoDBQueryRequest { - const { nextToken, limit = 500 } = ctx.args - const { id } = ctx.args.input + const { nextToken, limit = 500 } = ctx.args; + const { id } = ctx.args.input; return ddb.query({ query: { dataFeedId: { eq: id }, - sk: { beginsWith: 'article' } + sk: { beginsWith: 'article' }, }, limit, - nextToken - }) + nextToken, + }); } export function response (ctx: Context): any { - const skPrefix = 'article' + const skPrefix = 'article'; if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } const articles = { items: ctx.result.items.map((item: any) => { - item.id = item.articleId.substring(skPrefix.length) + item.id = item.articleId.substring(skPrefix.length); item.account = { - id: item.accountId - } - return item + id: item.accountId, + }; + return item; }), - nextToken: ctx.result.nextToken as string - } + nextToken: ctx.result.nextToken as string, + }; if (ctx.info.fieldName === 'getDataFeed') { - const result = ctx.prev.result - result.items = articles - return result + const result = ctx.prev.result; + result.items = articles; + return result; } - return articles + return articles; } diff --git a/lib/api/functions/pipeline/listDataFeedsById/index.ts b/lib/api/functions/pipeline/listDataFeedsById/index.ts index 0dc9b13..0f8fe09 100644 --- a/lib/api/functions/pipeline/listDataFeedsById/index.ts +++ b/lib/api/functions/pipeline/listDataFeedsById/index.ts @@ -1,22 +1,22 @@ import { type Context, util, - type DynamoDBBatchGetItemRequest -} from '@aws-appsync/utils' + type DynamoDBBatchGetItemRequest, +} from '@aws-appsync/utils'; export function request (ctx: Context): DynamoDBBatchGetItemRequest { - const { NEWSLETTER_TABLE } = ctx.env + const { NEWSLETTER_TABLE } = ctx.env; return { operation: 'BatchGetItem', tables: { [NEWSLETTER_TABLE]: ctx.args.dataFeedIds.map((dataFeedId: string) => - util.dynamodb.toMapValues({ dataFeedId }) - ) - } - } + util.dynamodb.toMapValues({ dataFeedId }), + ), + }, + }; } export function response (ctx: Context): any { - ctx.prev.result.dataFeeds = ctx.result?.items - return ctx.prev.result + ctx.prev.result.dataFeeds = ctx.result?.items; + return ctx.prev.result; } diff --git a/lib/api/functions/pipeline/listDataFeedsDiscoverable/index.ts b/lib/api/functions/pipeline/listDataFeedsDiscoverable/index.ts index 919c7c0..e2d949a 100644 --- a/lib/api/functions/pipeline/listDataFeedsDiscoverable/index.ts +++ b/lib/api/functions/pipeline/listDataFeedsDiscoverable/index.ts @@ -2,52 +2,52 @@ import { type Context, util, runtime, - type DynamoDBQueryRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBQueryRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBQueryRequest { - const dataFeedTypeIndex = 'type-index' // TODO - Make ENV variable - const input = ctx.args.input + const dataFeedTypeIndex = 'type-index'; // TODO - Make ENV variable + const input = ctx.args.input; const includeDiscoverable = input?.includeDiscoverable !== undefined ? input.includeDiscoverable - : ctx.stash.lookupDefinition.includeDiscoverable ?? false + : ctx.stash.lookupDefinition.includeDiscoverable ?? false; if (includeDiscoverable === false) { - runtime.earlyReturn(ctx.prev.result) + runtime.earlyReturn(ctx.prev.result); } - const { nextToken, limit = 1000 } = ctx.args + const { nextToken, limit = 1000 } = ctx.args; return ddb.query({ query: { sk: { eq: 'dataFeed' } }, filter: { - isPrivate: { eq: false } + isPrivate: { eq: false }, }, index: dataFeedTypeIndex, limit, nextToken, - select: 'ALL_ATTRIBUTES' - }) + select: 'ALL_ATTRIBUTES', + }); } export function response (ctx: Context): any { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - let result = ctx.result - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'dataFeedId') + let result = ctx.result; + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'dataFeedId'); if (ctx.prev?.result?.items !== undefined && result.items !== undefined) { - result.items.push(...ctx.prev.result.items) + result.items.push(...ctx.prev.result.items); } else if (ctx.prev?.result?.items !== undefined) { - result.items = [...ctx.prev.result.items] + result.items = [...ctx.prev.result.items]; } if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } - return result + return result; } diff --git a/lib/api/functions/pipeline/listDataFeedsOwned/index.ts b/lib/api/functions/pipeline/listDataFeedsOwned/index.ts index 99cf7b7..4dc7516 100644 --- a/lib/api/functions/pipeline/listDataFeedsOwned/index.ts +++ b/lib/api/functions/pipeline/listDataFeedsOwned/index.ts @@ -8,54 +8,54 @@ import { type Context, util, type DynamoDBQueryRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' -const dataFeedTypeIndex = 'type-index' // TODO - Make ENV variable + filterForDuplicatesById, +} from '../../resolver-helper'; +const dataFeedTypeIndex = 'type-index'; // TODO - Make ENV variable export function request (ctx: Context): DynamoDBQueryRequest { - const identity = ctx.identity as AppSyncIdentityLambda - const input = ctx.args.input + const identity = ctx.identity as AppSyncIdentityLambda; + const input = ctx.args.input; const includeOwned = input?.includeOwned !== undefined ? input.includeOwned - : ctx.stash.lookupDefinition.includeOwned ?? true + : ctx.stash.lookupDefinition.includeOwned ?? true; if (includeOwned === false) { - runtime.earlyReturn(ctx.prev.result) + runtime.earlyReturn(ctx.prev.result); } - const { nextToken, limit = 1000 } = ctx.args + const { nextToken, limit = 1000 } = ctx.args; return ddb.query({ query: { sk: { eq: 'dataFeed' }, - accountId: { eq: identity.resolverContext.accountId } + accountId: { eq: identity.resolverContext.accountId }, }, index: dataFeedTypeIndex, limit, nextToken, - select: 'ALL_ATTRIBUTES' - }) + select: 'ALL_ATTRIBUTES', + }); } export function response (ctx: Context): any { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - let result = ctx.result - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'dataFeedId') + let result = ctx.result; + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'dataFeedId'); if (ctx.prev?.result?.items !== undefined && result.items !== undefined) { - result.items.push(...ctx.prev.result.items) + result.items.push(...ctx.prev.result.items); } else if (ctx.prev?.result?.items !== undefined) { - result.items = [...ctx.prev.result.items] + result.items = [...ctx.prev.result.items]; } if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } - return result + return result; } diff --git a/lib/api/functions/pipeline/listDataFeedsShared/index.ts b/lib/api/functions/pipeline/listDataFeedsShared/index.ts index 8326640..a606b67 100644 --- a/lib/api/functions/pipeline/listDataFeedsShared/index.ts +++ b/lib/api/functions/pipeline/listDataFeedsShared/index.ts @@ -2,49 +2,49 @@ import { type Context, util, runtime, - type DynamoDBQueryRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBQueryRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBQueryRequest { - const dataFeedTypeIndex = 'type-index' // TODO - Make ENV variable - const input = ctx.args.input + const dataFeedTypeIndex = 'type-index'; // TODO - Make ENV variable + const input = ctx.args.input; const includeShared = input?.includeShared !== undefined ? input.includeShared - : ctx.stash.lookupDefinition.includeShared ?? false + : ctx.stash.lookupDefinition.includeShared ?? false; if (includeShared === false) { - runtime.earlyReturn(ctx.prev.result) + runtime.earlyReturn(ctx.prev.result); } - const { nextToken, limit = 1000 } = ctx.args + const { nextToken, limit = 1000 } = ctx.args; return ddb.query({ query: { sk: { eq: 'dataFeed' } }, index: dataFeedTypeIndex, limit, nextToken, - select: 'ALL_ATTRIBUTES' - }) + select: 'ALL_ATTRIBUTES', + }); } export function response (ctx: Context): any { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - let result = ctx.result - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'dataFeedId') + let result = ctx.result; + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'dataFeedId'); if (ctx.prev?.result?.items !== undefined && result.items !== undefined) { - result.items.push(...ctx.prev.result.items) + result.items.push(...ctx.prev.result.items); } else if (ctx.prev?.result?.items !== undefined) { - result.items = [...ctx.prev.result.items] + result.items = [...ctx.prev.result.items]; } if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } - return result + return result; } diff --git a/lib/api/functions/pipeline/listNewslettersById/index.ts b/lib/api/functions/pipeline/listNewslettersById/index.ts index f86b5c0..1f93306 100644 --- a/lib/api/functions/pipeline/listNewslettersById/index.ts +++ b/lib/api/functions/pipeline/listNewslettersById/index.ts @@ -1,45 +1,45 @@ import { type Context, util, - type DynamoDBBatchGetItemRequest -} from '@aws-appsync/utils' + type DynamoDBBatchGetItemRequest, +} from '@aws-appsync/utils'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBBatchGetItemRequest { - const { NEWSLETTER_TABLE } = ctx.env + const { NEWSLETTER_TABLE } = ctx.env; const newsletterIds = - ctx.args.newsletterIds ?? ctx.prev.result.newsletterIds ?? undefined + ctx.args.newsletterIds ?? ctx.prev.result.newsletterIds ?? undefined; if (newsletterIds === undefined) { - util.error('No newsletter Ids defined', 'ValidationException') + util.error('No newsletter Ids defined', 'ValidationException'); } if (newsletterIds.length === 0) { - runtime.earlyReturn([]) + runtime.earlyReturn([]); } return { operation: 'BatchGetItem', tables: { [NEWSLETTER_TABLE]: { keys: newsletterIds.map((newsletterId: string) => - util.dynamodb.toMapValues({ newsletterId, sk: 'newsletter' }) - ) - } - } - } + util.dynamodb.toMapValues({ newsletterId, sk: 'newsletter' }), + ), + }, + }, + }; } export function response (ctx: Context): any { - const { NEWSLETTER_TABLE } = ctx.env + const { NEWSLETTER_TABLE } = ctx.env; let result = { - items: ctx.result?.data[NEWSLETTER_TABLE] ?? [] - } - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'newsletterId') + items: ctx.result?.data[NEWSLETTER_TABLE] ?? [], + }; + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'newsletterId'); if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } - return result + return result; } diff --git a/lib/api/functions/pipeline/listNewslettersDiscoverable/index.ts b/lib/api/functions/pipeline/listNewslettersDiscoverable/index.ts index ec096ca..e33360a 100644 --- a/lib/api/functions/pipeline/listNewslettersDiscoverable/index.ts +++ b/lib/api/functions/pipeline/listNewslettersDiscoverable/index.ts @@ -2,58 +2,58 @@ import { type Context, util, runtime, - type DynamoDBQueryRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBQueryRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBQueryRequest { - const tableSKIndex = 'newsletter-item-type-index' // CDK doesn't have env variables yet - const { nextToken, limit = 500 } = ctx.args - const input = ctx.args.input + const tableSKIndex = 'newsletter-item-type-index'; // CDK doesn't have env variables yet + const { nextToken, limit = 500 } = ctx.args; + const input = ctx.args.input; const includeDiscoverable = input?.includeDiscoverable !== undefined ? input.includeDiscoverable - : ctx.stash.lookupDefinition.includeDiscoverable ?? false + : ctx.stash.lookupDefinition.includeDiscoverable ?? false; if (includeDiscoverable === true) { return ddb.query({ query: { - sk: { eq: 'newsletter' } + sk: { eq: 'newsletter' }, }, filter: { - isPrivate: { eq: false } + isPrivate: { eq: false }, }, index: tableSKIndex, limit, nextToken, - select: 'ALL_ATTRIBUTES' - }) + select: 'ALL_ATTRIBUTES', + }); } else { - runtime.earlyReturn(ctx.prev.result) + runtime.earlyReturn(ctx.prev.result); } } export function response (ctx: Context): any { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - let result = ctx.result - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'newsletterId') + let result = ctx.result; + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'newsletterId'); if (ctx.prev?.result?.items !== undefined && result.items !== undefined) { - result.items.push(...ctx.prev.result.items) + result.items.push(...ctx.prev.result.items); } else if (ctx.prev?.result?.items !== undefined) { - result.items = [...ctx.prev.result.items] + result.items = [...ctx.prev.result.items]; } if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } return { - items: result.items - } + items: result.items, + }; } diff --git a/lib/api/functions/pipeline/listNewslettersOwned/index.ts b/lib/api/functions/pipeline/listNewslettersOwned/index.ts index 166f6fc..d1eb8fb 100644 --- a/lib/api/functions/pipeline/listNewslettersOwned/index.ts +++ b/lib/api/functions/pipeline/listNewslettersOwned/index.ts @@ -3,56 +3,56 @@ import { util, runtime, type DynamoDBQueryRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBQueryRequest { - const identity = ctx.identity as AppSyncIdentityLambda - const tableSKIndex = 'newsletter-item-type-index' // CDK doesn't have env variables yet - const { nextToken, limit = 1000 } = ctx.args - const input = ctx.args.input + const identity = ctx.identity as AppSyncIdentityLambda; + const tableSKIndex = 'newsletter-item-type-index'; // CDK doesn't have env variables yet + const { nextToken, limit = 1000 } = ctx.args; + const input = ctx.args.input; const includeOwned = input?.includeOwned !== undefined ? input.includeOwned - : ctx.stash.lookupDefinition.includeOwned ?? (false as boolean) + : ctx.stash.lookupDefinition.includeOwned ?? (false as boolean); if (includeOwned === true) { return ddb.query({ query: { sk: { eq: 'newsletter' }, - accountId: { eq: identity.resolverContext.accountId } + accountId: { eq: identity.resolverContext.accountId }, }, index: tableSKIndex, limit, nextToken, - select: 'ALL_ATTRIBUTES' - }) + select: 'ALL_ATTRIBUTES', + }); } else { - runtime.earlyReturn(ctx.prev.result) + runtime.earlyReturn(ctx.prev.result); } } export function response (ctx: Context): any { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - let result = ctx.result - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'newsletterId') + let result = ctx.result; + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'newsletterId'); if (ctx.prev?.result?.items !== undefined && result.items !== undefined) { - result.items.push(...ctx.prev.result.items) + result.items.push(...ctx.prev.result.items); } else if (ctx.prev?.result?.items !== undefined) { - result.items = [...ctx.prev.result.items] + result.items = [...ctx.prev.result.items]; } if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } return { - items: result.items - } + items: result.items, + }; } diff --git a/lib/api/functions/pipeline/listNewslettersShared/index.ts b/lib/api/functions/pipeline/listNewslettersShared/index.ts index 45b3bba..bebc464 100644 --- a/lib/api/functions/pipeline/listNewslettersShared/index.ts +++ b/lib/api/functions/pipeline/listNewslettersShared/index.ts @@ -3,60 +3,60 @@ import { util, runtime, type DynamoDBQueryRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBQueryRequest { // const { tableSKIndex } = ctx.env - const identity = ctx.identity as AppSyncIdentityLambda - const tableSKIndex = 'newsletter-item-type-index' // CDK doesn't have env variables yet - const { nextToken, limit = 500 } = ctx.args - const input = ctx.args.input + const identity = ctx.identity as AppSyncIdentityLambda; + const tableSKIndex = 'newsletter-item-type-index'; // CDK doesn't have env variables yet + const { nextToken, limit = 500 } = ctx.args; + const input = ctx.args.input; const includeShared = input?.includeShared !== undefined ? input.includeShared - : ctx.stash.lookupDefinition.includeShared ?? false + : ctx.stash.lookupDefinition.includeShared ?? false; if (includeShared === true) { return ddb.query({ query: { - sk: { eq: 'newsletter' } + sk: { eq: 'newsletter' }, }, filter: { - sharedWith: { contains: identity.resolverContext.accountId } + sharedWith: { contains: identity.resolverContext.accountId }, }, index: tableSKIndex, limit, nextToken, - select: 'ALL_ATTRIBUTES' - }) + select: 'ALL_ATTRIBUTES', + }); } else { - runtime.earlyReturn(ctx.prev.result) + runtime.earlyReturn(ctx.prev.result); } } export function response (ctx: Context): any { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - let result = ctx.result - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'newsletterId') + let result = ctx.result; + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'newsletterId'); if (ctx.prev?.result?.items !== undefined && result.items !== undefined) { - result.items.push(...ctx.prev.result.items) + result.items.push(...ctx.prev.result.items); } else if (ctx.prev?.result?.items !== undefined) { - result.items = [...ctx.prev.result.items] + result.items = [...ctx.prev.result.items]; } if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } return { - items: result.items - } + items: result.items, + }; } diff --git a/lib/api/functions/pipeline/listPublications/index.ts b/lib/api/functions/pipeline/listPublications/index.ts index 4fdf48f..cbcdc68 100644 --- a/lib/api/functions/pipeline/listPublications/index.ts +++ b/lib/api/functions/pipeline/listPublications/index.ts @@ -1,57 +1,57 @@ import { type Context, util, - type DynamoDBQueryRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBQueryRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItem, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBQueryRequest { - const { nextToken, limit = 250 } = ctx.args - const { id } = ctx.args.input + const { nextToken, limit = 250 } = ctx.args; + const { id } = ctx.args.input; return ddb.query({ query: { newsletterId: { eq: id }, - sk: { beginsWith: 'publication' } + sk: { beginsWith: 'publication' }, }, limit, nextToken, - consistentRead: false - }) + consistentRead: false, + }); } export const response = (ctx: Context): any => { if (ctx.error !== undefined && ctx.error !== null) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - const items: any[] = [] + const items: any[] = []; if (ctx.result.items !== undefined) { for (const item of ctx.result.items) { - const { emailKey, createdAt, newsletterId, sk, accountId } = item - const publicationId = sk.split('#')[1] - let filePath = '' + const { emailKey, createdAt, newsletterId, sk, accountId } = item; + const publicationId = sk.split('#')[1]; + let filePath = ''; if (emailKey === undefined) { const epochCreatedAt = util.time.parseISO8601ToEpochMilliSeconds( - createdAt as string - ) + createdAt as string, + ); const year = util.time.epochMilliSecondsToFormatted( epochCreatedAt, - 'YYYY' - ) + 'YYYY', + ); const month = util.time.epochMilliSecondsToFormatted( epochCreatedAt, - 'MM' - ) - const day = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'DD') - const publicationId = sk.split('#')[1] - filePath = `/newsletter-content/${year}/${month}/${day}/${publicationId}` + 'MM', + ); + const day = util.time.epochMilliSecondsToFormatted(epochCreatedAt, 'DD'); + const publicationIdStr = sk.split('#')[1]; + filePath = `/newsletter-content/${year}/${month}/${day}/${publicationIdStr}`; } else { - filePath = emailKey + filePath = emailKey; if (filePath.indexOf('/') !== 0) { - filePath = '/' + filePath + filePath = '/' + filePath; } } let itemToPush = { @@ -59,20 +59,20 @@ export const response = (ctx: Context): any => { accountId, id: publicationId, createdAt, - filePath - } - itemToPush = addAccountToItem(itemToPush) - items.push(itemToPush) + filePath, + }; + itemToPush = addAccountToItem(itemToPush); + items.push(itemToPush); } } let result = { - items - } + items, + }; if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } return { items: result.items, - nextToken: ctx.result.nextToken - } -} + nextToken: ctx.result.nextToken, + }; +}; diff --git a/lib/api/functions/pipeline/listUserSubscriptions/index.ts b/lib/api/functions/pipeline/listUserSubscriptions/index.ts index 5100582..24a9062 100644 --- a/lib/api/functions/pipeline/listUserSubscriptions/index.ts +++ b/lib/api/functions/pipeline/listUserSubscriptions/index.ts @@ -1,43 +1,43 @@ import { type DynamoDBQueryRequest, type Context, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; import { addAccountToItems, convertFieldIdsToObjectIds, - filterForDuplicatesById -} from '../../resolver-helper' + filterForDuplicatesById, +} from '../../resolver-helper'; export function request (ctx: Context): DynamoDBQueryRequest { - const identity = ctx.identity as AppSyncIdentityLambda - const { NEWSLETTER_TABLE_ITEM_TYPE_GSI } = ctx.env + const identity = ctx.identity as AppSyncIdentityLambda; + const { NEWSLETTER_TABLE_ITEM_TYPE_GSI } = ctx.env; return ddb.query({ index: NEWSLETTER_TABLE_ITEM_TYPE_GSI, query: { - sk: { eq: 'subscriber#' + identity.resolverContext.userId } + sk: { eq: 'subscriber#' + identity.resolverContext.userId }, }, - consistentRead: false - }) + consistentRead: false, + }); } export const response = (ctx: Context): any => { - let { result } = ctx + let { result } = ctx; if (result.items === undefined || result.items === null) { - runtime.earlyReturn([]) + runtime.earlyReturn([]); } - result = addAccountToItems(result) - result = convertFieldIdsToObjectIds(result, 'newsletterId') + result = addAccountToItems(result); + result = convertFieldIdsToObjectIds(result, 'newsletterId'); if (ctx.prev?.result?.items !== undefined && result.items !== undefined) { - result.items.push(...ctx.prev.result.items) + result.items.push(...ctx.prev.result.items); } else if (ctx.prev?.result?.items !== undefined) { - result.items = [...ctx.prev.result.items] + result.items = [...ctx.prev.result.items]; } if (result.items !== undefined) { - result = filterForDuplicatesById(result) + result = filterForDuplicatesById(result); } return { - items: result.items ?? [] - } -} + items: result.items ?? [], + }; +}; diff --git a/lib/api/functions/pipeline/subscribeToNewsletter/index.ts b/lib/api/functions/pipeline/subscribeToNewsletter/index.ts index ed375a6..cb6fa85 100644 --- a/lib/api/functions/pipeline/subscribeToNewsletter/index.ts +++ b/lib/api/functions/pipeline/subscribeToNewsletter/index.ts @@ -2,30 +2,30 @@ import { type Context, util, type LambdaRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; export function request (ctx: Context): LambdaRequest { - const { args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda - const input = args.input + const { args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('Newsletter ID is required', 'ValidationException') + util.error('Newsletter ID is required', 'ValidationException'); } return { operation: 'Invoke', payload: { cognitoUserId: identity.resolverContext.userId, newsletterId: input.id, - accountId: identity.resolverContext.userId - } - } + accountId: identity.resolverContext.userId, + }, + }; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/pipeline/unsubscribeFromNewsletter/index.ts b/lib/api/functions/pipeline/unsubscribeFromNewsletter/index.ts index 802c984..24fb710 100644 --- a/lib/api/functions/pipeline/unsubscribeFromNewsletter/index.ts +++ b/lib/api/functions/pipeline/unsubscribeFromNewsletter/index.ts @@ -2,30 +2,30 @@ import { type Context, util, type LambdaRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; export function request (ctx: Context): LambdaRequest { - const { args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda - const input = args.input + const { args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('Newsletter ID is required', 'ValidationException') + util.error('Newsletter ID is required', 'ValidationException'); } return { operation: 'Invoke', payload: { cognitoUserId: identity.resolverContext.userId, newsletterId: input.id, - accountId: identity.resolverContext.accountId - } - } + accountId: identity.resolverContext.accountId, + }, + }; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/pipeline/updateDataFeed/index.ts b/lib/api/functions/pipeline/updateDataFeed/index.ts index 9fbc7a0..360d8b4 100644 --- a/lib/api/functions/pipeline/updateDataFeed/index.ts +++ b/lib/api/functions/pipeline/updateDataFeed/index.ts @@ -1,33 +1,33 @@ import { type Context, util, - type DynamoDBUpdateItemRequest -} from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' + type DynamoDBUpdateItemRequest, +} from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; export function request (ctx: Context): DynamoDBUpdateItemRequest { - const values: Record = {} + const values: Record = {}; for (const [key, value] of Object.entries( - ctx.args.input as Record + ctx.args.input as Record, )) { if (key !== 'id' && value !== undefined && value !== null) { - values[key] = value + values[key] = value; } } return ddb.update({ key: { dataFeedId: ctx.args.input.id, - sk: 'dataFeed' + sk: 'dataFeed', }, - update: { ...values } - }) + update: { ...values }, + }); } export function response (ctx: Context): any { - const { error } = ctx + const { error } = ctx; if (error !== undefined && error !== null) { - util.error(error.message, error.type) + util.error(error.message, error.type); } else { - return true + return true; } } diff --git a/lib/api/functions/pipeline/updateNewsletter/index.ts b/lib/api/functions/pipeline/updateNewsletter/index.ts index 91802b3..4f010f2 100644 --- a/lib/api/functions/pipeline/updateNewsletter/index.ts +++ b/lib/api/functions/pipeline/updateNewsletter/index.ts @@ -1,91 +1,91 @@ import { type Context, util, - type DynamoDBUpdateItemRequest -} from '@aws-appsync/utils' + type DynamoDBUpdateItemRequest, +} from '@aws-appsync/utils'; export function request (ctx: Context): DynamoDBUpdateItemRequest { - const input = ctx.args.input - let expression = 'SET ' - const expressionNames: Record = {} - const expressionValues: Record = {} - let updates = 0 + const input = ctx.args.input; + let expression = 'SET '; + const expressionNames: Record = {}; + const expressionValues: Record = {}; + let updates = 0; if (input.title != null) { - expression += '#title = :title, ' - expressionNames['#title'] = 'title' - expressionValues[':title'] = util.dynamodb.toDynamoDB(input.title) - updates = updates + 1 + expression += '#title = :title, '; + expressionNames['#title'] = 'title'; + expressionValues[':title'] = util.dynamodb.toDynamoDB(input.title); + updates = updates + 1; } if (input.numberOfDaysToInclude != null) { - expression += '#numberOfDaysToInclude = :numberOfDaysToInclude, ' - expressionNames['#numberOfDaysToInclude'] = 'numberOfDaysToInclude' + expression += '#numberOfDaysToInclude = :numberOfDaysToInclude, '; + expressionNames['#numberOfDaysToInclude'] = 'numberOfDaysToInclude'; expressionValues[':numberOfDaysToInclude'] = util.dynamodb.toDynamoDB( - input.numberOfDaysToInclude - ) - updates = updates + 1 + input.numberOfDaysToInclude, + ); + updates = updates + 1; } if (input.dataFeeds != null) { - expression += '#dataFeedIds = :dataFeedIds, ' - expressionNames['#dataFeedIds'] = 'dataFeedIds' + expression += '#dataFeedIds = :dataFeedIds, '; + expressionNames['#dataFeedIds'] = 'dataFeedIds'; expressionValues[':dataFeedIds'] = util.dynamodb.toDynamoDB([ - ...input.dataFeeds - ]) - updates = updates + 1 + ...input.dataFeeds, + ]); + updates = updates + 1; } if (input.isPrivate !== null) { - expression += '#isPrivate = :isPrivate, ' - expressionNames['#isPrivate'] = 'isPrivate' + expression += '#isPrivate = :isPrivate, '; + expressionNames['#isPrivate'] = 'isPrivate'; expressionValues[':isPrivate'] = util.dynamodb.toDynamoDB( - input.isPrivate ?? true - ) - updates = updates + 1 + input.isPrivate ?? true, + ); + updates = updates + 1; } if (input.newsletterIntroPrompt != null) { - expression += '#newsletterIntroPrompt = :newsletterIntroPrompt, ' - expressionNames['#newsletterIntroPrompt'] = 'newsletterIntroPrompt' + expression += '#newsletterIntroPrompt = :newsletterIntroPrompt, '; + expressionNames['#newsletterIntroPrompt'] = 'newsletterIntroPrompt'; expressionValues[':newsletterIntroPrompt'] = util.dynamodb.toDynamoDB( - input.newsletterIntroPrompt - ) - updates = updates + 1 + input.newsletterIntroPrompt, + ); + updates = updates + 1; } if (input.newsletterStyle != null) { - expression += '#newsletterStyle = :newsletterStyle, ' - expressionNames['#newsletterStyle'] = 'newsletterStyle' + expression += '#newsletterStyle = :newsletterStyle, '; + expressionNames['#newsletterStyle'] = 'newsletterStyle'; expressionValues[':newsletterStyle'] = util.dynamodb.toDynamoDB( - input.newsletterStyle - ) - updates = updates + 1 + input.newsletterStyle, + ); + updates = updates + 1; } if (input.articleSummaryType != null) { - expression += '#articleSummaryType = :articleSummaryType, ' - expressionNames['#articleSummaryType'] = 'articleSummaryType' + expression += '#articleSummaryType = :articleSummaryType, '; + expressionNames['#articleSummaryType'] = 'articleSummaryType'; expressionValues[':articleSummaryType'] = util.dynamodb.toDynamoDB( - input.articleSummaryType - ) - updates = updates + 1 + input.articleSummaryType, + ); + updates = updates + 1; } if (updates > 0) { return { operation: 'UpdateItem', key: { newsletterId: util.dynamodb.toDynamoDB(input.id), - sk: util.dynamodb.toDynamoDB('newsletter') + sk: util.dynamodb.toDynamoDB('newsletter'), }, update: { expression: expression.trim().replace(',+$', ''), expressionNames, - expressionValues - } - } + expressionValues, + }, + }; } - util.error(`No updates to perform for newsletter ${input.id}`) + util.error(`No updates to perform for newsletter ${input.id}`); } export function response (ctx: Context): unknown { - const { error } = ctx + const { error } = ctx; if (error !== undefined && error !== null) { - util.error(error.message, error.type) + util.error(error.message, error.type); } else { - return true + return true; } } diff --git a/lib/api/functions/resolver-helper.ts b/lib/api/functions/resolver-helper.ts index 7469a9b..c1f40a6 100644 --- a/lib/api/functions/resolver-helper.ts +++ b/lib/api/functions/resolver-helper.ts @@ -1,4 +1,4 @@ -import { type Context, runtime } from '@aws-appsync/utils' +import { type Context, runtime } from '@aws-appsync/utils'; /** * Adds account to item and removes accountId * @returns @@ -6,12 +6,12 @@ import { type Context, runtime } from '@aws-appsync/utils' export const addAccountToItem = (obj: any): any => { if (obj === undefined) { - return obj + return obj; } - obj.account = { id: obj.accountId, __typename: 'Account' } - delete obj.accountId - return obj -} + obj.account = { id: obj.accountId, __typename: 'Account' }; + delete obj.accountId; + return obj; +}; /** * Converts the object created for AVP to the GraphQL expected shape @@ -20,14 +20,14 @@ export const addAccountToItem = (obj: any): any => { */ export const convertAvpObjectToGraphql = (obj: any): any => { if (obj === undefined) { - return obj + return obj; } if (obj.Account !== undefined) { - obj.account = obj.Account - delete obj.Account + obj.account = obj.Account; + delete obj.Account; } - return obj -} + return obj; +}; /** * Converts the objects created for AVP to the GraphQL expected shape @@ -36,12 +36,12 @@ export const convertAvpObjectToGraphql = (obj: any): any => { */ export const convertAvpObjectsToGraphql = (obj: any): any => { if (obj === undefined || obj.items === undefined) { - return obj + return obj; } return obj.items.map((item: any) => { - return convertAvpObjectToGraphql(item) - }) -} + return convertAvpObjectToGraphql(item); + }); +}; /** * Converts field id to object's "id" field and removes the provided id field @@ -51,16 +51,16 @@ export const convertAvpObjectsToGraphql = (obj: any): any => { */ export const convertFieldIdToObjectId = ( obj: any, - idFieldName: string + idFieldName: string, ): any => { if (obj === undefined) { - return obj + return obj; } - obj.id = obj[idFieldName] + obj.id = obj[idFieldName]; - delete obj[idFieldName] - return obj -} + delete obj[idFieldName]; + return obj; +}; /** * Converts field id to object @@ -71,19 +71,19 @@ export const convertFieldIdToObjectId = ( export const convertFieldIdToObject = ( obj: any, fieldIdName: string, - objectName: string + objectName: string, ): any => { if (obj === undefined) { - return obj + return obj; } obj[objectName] = { __typename: objectName, - id: obj[fieldIdName] - } + id: obj[fieldIdName], + }; - delete obj[fieldIdName] - return obj -} + delete obj[fieldIdName]; + return obj; +}; /** * Filters items for duplicates by id @@ -92,18 +92,18 @@ export const convertFieldIdToObject = ( */ export const filterForDuplicatesById = (obj: any): any => { if (obj === undefined || obj.items === undefined) { - return obj + return obj; } return { items: obj.items.filter( (item: { id: any }, index: any, itemArray: any[]) => { return ( itemArray.findIndex((i: { id: any }) => i.id === item.id) === index - ) - } - ) - } -} + ); + }, + ), + }; +}; /** * Converts field id to object's "id" field and removes the provided id field @@ -112,17 +112,17 @@ export const filterForDuplicatesById = (obj: any): any => { */ export const convertFieldIdsToObjectIds = ( obj: any, - idFieldName: string + idFieldName: string, ): any => { if (obj === undefined || obj.items === undefined) { - return obj + return obj; } return { items: obj.items.map((item: any) => { - return convertFieldIdToObjectId(item, idFieldName) - }) - } -} + return convertFieldIdToObjectId(item, idFieldName); + }), + }; +}; /** * converts the items in obj from string field to object with id @@ -134,17 +134,17 @@ export const convertFieldIdsToObjectIds = ( export const convertFieldIdsToObjects = ( obj: any, idFieldName: string, - objectName: string + objectName: string, ): any => { if (obj === undefined || obj.items === undefined) { - return obj + return obj; } return { items: obj.items.map((item: any) => { - return convertFieldIdToObject(item, idFieldName, objectName) - }) - } -} + return convertFieldIdToObject(item, idFieldName, objectName); + }), + }; +}; /** * Add account to items @@ -152,19 +152,19 @@ export const convertFieldIdsToObjects = ( */ export const addAccountToItems = (obj: any): any => { if (obj === undefined || obj.items === undefined) { - return obj + return obj; } return { items: obj.items.map((item: any) => { - return addAccountToItem(item) - }) - } -} + return addAccountToItem(item); + }), + }; +}; export const dryRunCheck = (ctx: Context): void => { if (ctx.arguments.actionAuthOnly === true) { runtime.earlyReturn({ - actionAuth: true - }) + actionAuth: true, + }); } -} +}; diff --git a/lib/api/functions/resolver/canUpdateDataFeed/index.ts b/lib/api/functions/resolver/canUpdateDataFeed/index.ts index 1d125ec..59bb68d 100644 --- a/lib/api/functions/resolver/canUpdateDataFeed/index.ts +++ b/lib/api/functions/resolver/canUpdateDataFeed/index.ts @@ -1,14 +1,14 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'DataFeed' - ctx.stash.contingentAction = 'updateDataFeed' - return {} + ctx.stash.root = 'DataFeed'; + ctx.stash.contingentAction = 'updateDataFeed'; + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return true + return true; } diff --git a/lib/api/functions/resolver/canUpdateNewsletter/index.ts b/lib/api/functions/resolver/canUpdateNewsletter/index.ts index ab92817..6e2ac99 100644 --- a/lib/api/functions/resolver/canUpdateNewsletter/index.ts +++ b/lib/api/functions/resolver/canUpdateNewsletter/index.ts @@ -1,14 +1,14 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'Newsletter' - ctx.stash.contingentAction = 'updateNewsletter' - return {} + ctx.stash.root = 'Newsletter'; + ctx.stash.contingentAction = 'updateNewsletter'; + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return true + return true; } diff --git a/lib/api/functions/resolver/checkSubscriptionToNewsletter/index.ts b/lib/api/functions/resolver/checkSubscriptionToNewsletter/index.ts index d933e3a..7f2eb51 100644 --- a/lib/api/functions/resolver/checkSubscriptionToNewsletter/index.ts +++ b/lib/api/functions/resolver/checkSubscriptionToNewsletter/index.ts @@ -1,18 +1,18 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): void { - const { args } = ctx - ctx.stash.root = 'Newsletter' - const input = args.input + const { args } = ctx; + ctx.stash.root = 'Newsletter'; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('Newsletter ID is required', 'ValidationException') + util.error('Newsletter ID is required', 'ValidationException'); } } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/createDataFeed/index.ts b/lib/api/functions/resolver/createDataFeed/index.ts index ec2af56..9494d33 100644 --- a/lib/api/functions/resolver/createDataFeed/index.ts +++ b/lib/api/functions/resolver/createDataFeed/index.ts @@ -1,13 +1,13 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'DataFeed' - return {} + ctx.stash.root = 'DataFeed'; + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/createNewsletter/index.ts b/lib/api/functions/resolver/createNewsletter/index.ts index b2d06f6..b4dd2df 100644 --- a/lib/api/functions/resolver/createNewsletter/index.ts +++ b/lib/api/functions/resolver/createNewsletter/index.ts @@ -8,29 +8,29 @@ import { type Context, util, type LambdaRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; export function request (ctx: Context): LambdaRequest { - ctx.stash.root = 'Newsletter' - const { args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda - const input = args.input + ctx.stash.root = 'Newsletter'; + const { args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; + const input = args.input; return { operation: 'Invoke', payload: { input, createdBy: { accountId: identity.resolverContext.accountId, - userId: identity.resolverContext.userId - } - } - } + userId: identity.resolverContext.userId, + }, + }, + }; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/externalUnsubscribeFromNewsletter/index.ts b/lib/api/functions/resolver/externalUnsubscribeFromNewsletter/index.ts index ca3ab77..5353f8b 100644 --- a/lib/api/functions/resolver/externalUnsubscribeFromNewsletter/index.ts +++ b/lib/api/functions/resolver/externalUnsubscribeFromNewsletter/index.ts @@ -1,26 +1,26 @@ -import { type Context, util, type LambdaRequest } from '@aws-appsync/utils' +import { type Context, util, type LambdaRequest } from '@aws-appsync/utils'; export function request (ctx: Context): LambdaRequest { - const { newsletterId, userId } = ctx.args.input + const { newsletterId, userId } = ctx.args.input; if (newsletterId === null || userId === null) { util.error( 'Newsletter ID & User ID are both required', - 'ValidationException' - ) + 'ValidationException', + ); } return { operation: 'Invoke', payload: { cognitoUserId: userId, - newsletterId - } - } + newsletterId, + }, + }; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return true + return true; } diff --git a/lib/api/functions/resolver/flagArticle/index.ts b/lib/api/functions/resolver/flagArticle/index.ts index f3e3407..f4af37a 100644 --- a/lib/api/functions/resolver/flagArticle/index.ts +++ b/lib/api/functions/resolver/flagArticle/index.ts @@ -1,31 +1,31 @@ -import { type Context, util } from '@aws-appsync/utils' -import * as ddb from '@aws-appsync/utils/dynamodb' +import { type Context, util } from '@aws-appsync/utils'; +import * as ddb from '@aws-appsync/utils/dynamodb'; export function request (ctx: Context): any { - ctx.stash.root = 'Article' - const { args } = ctx - const input = args.input + ctx.stash.root = 'Article'; + const { args } = ctx; + const input = args.input; if (input.dataFeedId === undefined || input.dataFeedId === null) { - util.error('DataFeedID is required', 'ValidationException') + util.error('DataFeedID is required', 'ValidationException'); } if (input.id === undefined || input.id === null) { - util.error('ArticleID is required', 'ValidationException') + util.error('ArticleID is required', 'ValidationException'); } const flaggedUpdate = ddb.operations.replace({ - flaggedContent: true - }) + flaggedContent: true, + }); return ddb.update({ key: { dataFeedId: { eq: input.id }, - sk: { eq: 'article#' + input.id } + sk: { eq: 'article#' + input.id }, }, - update: flaggedUpdate - }) + update: flaggedUpdate, + }); } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return true + return true; } diff --git a/lib/api/functions/resolver/getDataFeed/index.ts b/lib/api/functions/resolver/getDataFeed/index.ts index 77072e0..c3dde0d 100644 --- a/lib/api/functions/resolver/getDataFeed/index.ts +++ b/lib/api/functions/resolver/getDataFeed/index.ts @@ -1,18 +1,18 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - const { args } = ctx - ctx.stash.root = 'DataFeed' - const input = args.input + const { args } = ctx; + ctx.stash.root = 'DataFeed'; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('DataFeedID is required', 'ValidationException') + util.error('DataFeedID is required', 'ValidationException'); } - return {} + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/getNewsletter/index.ts b/lib/api/functions/resolver/getNewsletter/index.ts index 4fd2b02..3e95b24 100644 --- a/lib/api/functions/resolver/getNewsletter/index.ts +++ b/lib/api/functions/resolver/getNewsletter/index.ts @@ -4,16 +4,16 @@ * SPDX-License-Identifier: MIT-0 */ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'Newsletter' - return {} + ctx.stash.root = 'Newsletter'; + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.prev.result + return ctx.prev.result; } diff --git a/lib/api/functions/resolver/getNewsletterSubscriberStats/index.ts b/lib/api/functions/resolver/getNewsletterSubscriberStats/index.ts index 4fd2b02..3e95b24 100644 --- a/lib/api/functions/resolver/getNewsletterSubscriberStats/index.ts +++ b/lib/api/functions/resolver/getNewsletterSubscriberStats/index.ts @@ -4,16 +4,16 @@ * SPDX-License-Identifier: MIT-0 */ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'Newsletter' - return {} + ctx.stash.root = 'Newsletter'; + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.prev.result + return ctx.prev.result; } diff --git a/lib/api/functions/resolver/getPublication/index.ts b/lib/api/functions/resolver/getPublication/index.ts index f93e56d..4792f5d 100644 --- a/lib/api/functions/resolver/getPublication/index.ts +++ b/lib/api/functions/resolver/getPublication/index.ts @@ -1,21 +1,21 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - const { args } = ctx - ctx.stash.root = 'Publication' - const input = args.input + const { args } = ctx; + ctx.stash.root = 'Publication'; + const input = args.input; if (input.newsletterId === undefined || input.newsletterId === null) { - util.error('NewsletterId is required', 'ValidationException') + util.error('NewsletterId is required', 'ValidationException'); } if (input.id === undefined || input.id === null) { - util.error('PublicationId is required', 'ValidationException') + util.error('PublicationId is required', 'ValidationException'); } - return {} + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/listArticles/index.ts b/lib/api/functions/resolver/listArticles/index.ts index ec2af56..9494d33 100644 --- a/lib/api/functions/resolver/listArticles/index.ts +++ b/lib/api/functions/resolver/listArticles/index.ts @@ -1,13 +1,13 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'DataFeed' - return {} + ctx.stash.root = 'DataFeed'; + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/listDataFeeds/index.ts b/lib/api/functions/resolver/listDataFeeds/index.ts index d699047..47dea0f 100644 --- a/lib/api/functions/resolver/listDataFeeds/index.ts +++ b/lib/api/functions/resolver/listDataFeeds/index.ts @@ -1,8 +1,8 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - const input = ctx.args.input - ctx.stash.root = 'DataFeeds' + const input = ctx.args.input; + ctx.stash.root = 'DataFeeds'; if ( input === undefined || (input.includeOwned === undefined && @@ -12,23 +12,23 @@ export function request (ctx: Context): any { ctx.stash.lookupDefinition = { includeOwned: true, includeShared: false, - includeDiscoverable: false - } + includeDiscoverable: false, + }; } else { ctx.stash.lookupDefinition = { includeOwned: input.includeOwned ?? false, includeShared: input.includeShared ?? false, - includeDiscoverable: input.includeDiscoverable ?? false - } + includeDiscoverable: input.includeDiscoverable ?? false, + }; } } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } return { nextToken: ctx.result?.nextToken, - items: ctx.result?.items ?? [] - } + items: ctx.result?.items ?? [], + }; } diff --git a/lib/api/functions/resolver/listNewsletters/index.ts b/lib/api/functions/resolver/listNewsletters/index.ts index ef22c51..117d9ff 100644 --- a/lib/api/functions/resolver/listNewsletters/index.ts +++ b/lib/api/functions/resolver/listNewsletters/index.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: MIT-0 */ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'Newsletters' - console.log('[listNewslettersResolverRequest]', { ctx }) - const input = ctx.args.input + ctx.stash.root = 'Newsletters'; + console.log('[listNewslettersResolverRequest]', { ctx }); + const input = ctx.args.input; if ( input === undefined || (input.includeDiscoverable === undefined && @@ -19,25 +19,25 @@ export function request (ctx: Context): any { ctx.stash.lookupDefinition = { includeOwned: true, includeShared: false, - includeDiscoverable: false - } + includeDiscoverable: false, + }; } else { ctx.stash.lookupDefinition = { includeOwned: input.includeOwned ?? false, includeShared: input.includeShared ?? false, - includeDiscoverable: input.includeDiscoverable ?? false - } + includeDiscoverable: input.includeDiscoverable ?? false, + }; } - return {} + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } return { nextToken: ctx.result.nextToken, - items: ctx.result.items ?? [] - } + items: ctx.result.items ?? [], + }; } diff --git a/lib/api/functions/resolver/listPublications/index.ts b/lib/api/functions/resolver/listPublications/index.ts index b09a5ba..3848c25 100644 --- a/lib/api/functions/resolver/listPublications/index.ts +++ b/lib/api/functions/resolver/listPublications/index.ts @@ -1,19 +1,19 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'Publications' - const input = ctx.args.input + ctx.stash.root = 'Publications'; + const input = ctx.args.input; if (input.id === undefined || input.id === null) { - util.error('NewsletterId is required', 'ValidationException') + util.error('NewsletterId is required', 'ValidationException'); } - return {} + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } return { - items: ctx.result?.items ?? [] - } + items: ctx.result?.items ?? [], + }; } diff --git a/lib/api/functions/resolver/listUserSubscriptions/index.ts b/lib/api/functions/resolver/listUserSubscriptions/index.ts index 2af62c6..0ad6303 100644 --- a/lib/api/functions/resolver/listUserSubscriptions/index.ts +++ b/lib/api/functions/resolver/listUserSubscriptions/index.ts @@ -1,15 +1,15 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'Newsletters' - return {} + ctx.stash.root = 'Newsletters'; + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } return { - items: ctx.result?.items ?? [] - } + items: ctx.result?.items ?? [], + }; } diff --git a/lib/api/functions/resolver/subscribeToNewsletter/index.ts b/lib/api/functions/resolver/subscribeToNewsletter/index.ts index d933e3a..7f2eb51 100644 --- a/lib/api/functions/resolver/subscribeToNewsletter/index.ts +++ b/lib/api/functions/resolver/subscribeToNewsletter/index.ts @@ -1,18 +1,18 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): void { - const { args } = ctx - ctx.stash.root = 'Newsletter' - const input = args.input + const { args } = ctx; + ctx.stash.root = 'Newsletter'; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('Newsletter ID is required', 'ValidationException') + util.error('Newsletter ID is required', 'ValidationException'); } } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/unsubscribeFromNewsletter/index.ts b/lib/api/functions/resolver/unsubscribeFromNewsletter/index.ts index aee08a4..881abe3 100644 --- a/lib/api/functions/resolver/unsubscribeFromNewsletter/index.ts +++ b/lib/api/functions/resolver/unsubscribeFromNewsletter/index.ts @@ -8,29 +8,29 @@ import { type Context, util, type LambdaRequest, - type AppSyncIdentityLambda -} from '@aws-appsync/utils' + type AppSyncIdentityLambda, +} from '@aws-appsync/utils'; export function request (ctx: Context): LambdaRequest { - ctx.stash.root = 'Newsletter' - const { args } = ctx - const identity = ctx.identity as AppSyncIdentityLambda - const input = args.input + ctx.stash.root = 'Newsletter'; + const { args } = ctx; + const identity = ctx.identity as AppSyncIdentityLambda; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('Newsletter ID is required', 'ValidationException') + util.error('Newsletter ID is required', 'ValidationException'); } return { operation: 'Invoke', payload: { cognitoUserId: identity.resolverContext.userId, - newsletterId: input.id - } - } + newsletterId: input.id, + }, + }; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return ctx.result + return ctx.result; } diff --git a/lib/api/functions/resolver/updateDataFeed/index.ts b/lib/api/functions/resolver/updateDataFeed/index.ts index 7e31dfc..be8cb84 100644 --- a/lib/api/functions/resolver/updateDataFeed/index.ts +++ b/lib/api/functions/resolver/updateDataFeed/index.ts @@ -1,18 +1,18 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'DataFeed' - const { args } = ctx - const input = args.input + ctx.stash.root = 'DataFeed'; + const { args } = ctx; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('DataFeedID is required', 'ValidationException') + util.error('DataFeedID is required', 'ValidationException'); } - return {} + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return true + return true; } diff --git a/lib/api/functions/resolver/updateNewsletter/index.ts b/lib/api/functions/resolver/updateNewsletter/index.ts index e3b9d11..674754c 100644 --- a/lib/api/functions/resolver/updateNewsletter/index.ts +++ b/lib/api/functions/resolver/updateNewsletter/index.ts @@ -1,18 +1,18 @@ -import { type Context, util } from '@aws-appsync/utils' +import { type Context, util } from '@aws-appsync/utils'; export function request (ctx: Context): any { - ctx.stash.root = 'Newsletter' - const { args } = ctx - const input = args.input + ctx.stash.root = 'Newsletter'; + const { args } = ctx; + const input = args.input; if (input.id === undefined || input.id === null) { - util.error('NewsletterId is required', 'ValidationException') + util.error('NewsletterId is required', 'ValidationException'); } - return {} + return {}; } export function response (ctx: Context): any { if (ctx.error !== undefined) { - util.error(ctx.error.message, ctx.error.type) + util.error(ctx.error.message, ctx.error.type); } - return true + return true; } diff --git a/lib/api/index.ts b/lib/api/index.ts index ec427be..2629dff 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -4,62 +4,58 @@ * SPDX-License-Identifier: MIT-0 */ +import * as path from 'path'; +import { Duration, Stack } from 'aws-cdk-lib'; import { AuthorizationType, Definition, FieldLogLevel, - GraphqlApi -} from 'aws-cdk-lib/aws-appsync' -import { Construct } from 'constructs' -import * as path from 'path' -import { ApiResolvers } from './resolvers' -import { type ITable, type Table } from 'aws-cdk-lib/aws-dynamodb' -import { type NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' -import { Duration, Stack } from 'aws-cdk-lib' -import { type CfnPolicyStore } from 'aws-cdk-lib/aws-verifiedpermissions' -import { type Bucket } from 'aws-cdk-lib/aws-s3' -import { RetentionDays } from 'aws-cdk-lib/aws-logs' -import { type IRole } from 'aws-cdk-lib/aws-iam' -import { fileURLToPath } from 'url' -import { dirname } from 'path' + GraphqlApi, +} from 'aws-cdk-lib/aws-appsync'; +import { type ITable, type Table } from 'aws-cdk-lib/aws-dynamodb'; +import { type IRole } from 'aws-cdk-lib/aws-iam'; +import { type NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { type Bucket } from 'aws-cdk-lib/aws-s3'; +import { type CfnPolicyStore } from 'aws-cdk-lib/aws-verifiedpermissions'; +import { Construct } from 'constructs'; +import { ApiResolvers } from './resolvers'; -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) export interface ApiProps { - userPoolId: string - unauthenticatedUserRole: IRole - dataFeedTable: Table - dataFeedTableTypeIndex: string - dataFeedTableLSI: string - newsletterTable: Table - newsletterTableItemTypeGSI: string - accountTable: ITable - accountTableUserIndex: string - avpPolicyStore: CfnPolicyStore - loggingBucket: Bucket - avpAuthorizerValidationRegex: string + userPoolId: string; + unauthenticatedUserRole?: IRole; + dataFeedTable: Table; + dataFeedTableTypeIndex: string; + dataFeedTableLSI: string; + newsletterTable: Table; + newsletterTableItemTypeGSI: string; + accountTable: ITable; + accountTableUserIndex: string; + avpPolicyStore: CfnPolicyStore; + loggingBucket: Bucket; + avpAuthorizerValidationRegex: string; functions: { - graphqlActionAuthorizerFunction: NodejsFunction - graphqlReadAuthorizerFunction: NodejsFunction - graphqlFilterReadAuthorizerFunction: NodejsFunction - createNewsletterFunction: NodejsFunction - userSubscriberFunction: NodejsFunction - userUnsubscriberFunction: NodejsFunction - feedSubscriberFunction: NodejsFunction - getNewsletterFunction: NodejsFunction - } + graphqlActionAuthorizerFunction: NodejsFunction; + graphqlReadAuthorizerFunction: NodejsFunction; + graphqlFilterReadAuthorizerFunction: NodejsFunction; + createNewsletterFunction: NodejsFunction; + userSubscriberFunction: NodejsFunction; + userUnsubscriberFunction: NodejsFunction; + feedSubscriberFunction: NodejsFunction; + getNewsletterFunction: NodejsFunction; + }; } export class API extends Construct { - public readonly graphqlApiUrl: string + public readonly graphqlApiUrl: string; constructor (scope: Construct, id: string, props: ApiProps) { - super(scope, id) + super(scope, id); const graphqlApi = new GraphqlApi(this, 'API', { name: Stack.of(this).stackName + 'GraphQLAPI', definition: Definition.fromFile( - path.join(__dirname, '..', 'shared', 'api', 'schema.graphql') + path.join(__dirname, '..', 'shared', 'api', 'schema.graphql'), ), authorizationConfig: { defaultAuthorization: { @@ -67,32 +63,32 @@ export class API extends Construct { lambdaAuthorizerConfig: { handler: props.functions.graphqlActionAuthorizerFunction, resultsCacheTtl: Duration.seconds(0), - validationRegex: props.avpAuthorizerValidationRegex - } + validationRegex: props.avpAuthorizerValidationRegex, + }, }, additionalAuthorizationModes: [ { - authorizationType: AuthorizationType.IAM - } - ] + authorizationType: AuthorizationType.IAM, + }, + ], }, environmentVariables: { DATA_FEED_TABLE: props.dataFeedTable.tableName, NEWSLETTER_TABLE: props.newsletterTable.tableName, NEWSLETTER_TABLE_ITEM_TYPE_GSI: props.newsletterTableItemTypeGSI, - ACCOUNT_TABLE: props.accountTable.tableName + ACCOUNT_TABLE: props.accountTable.tableName, }, logConfig: { fieldLogLevel: FieldLogLevel.ALL, - retention: RetentionDays.INFINITE + retention: RetentionDays.INFINITE, }, - xrayEnabled: true - }) + xrayEnabled: true, + }); new ApiResolvers(this, 'ApiResolvers', { api: graphqlApi, - ...props - }) - this.graphqlApiUrl = graphqlApi.graphqlUrl + ...props, + }); + this.graphqlApiUrl = graphqlApi.graphqlUrl; } } diff --git a/lib/api/resolvers.ts b/lib/api/resolvers.ts index 108079b..cc0e021 100644 --- a/lib/api/resolvers.ts +++ b/lib/api/resolvers.ts @@ -3,6 +3,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ +import * as path from 'path'; import { type GraphqlApi, AppsyncFunction, @@ -10,48 +11,43 @@ import { Resolver, LambdaDataSource, DynamoDbDataSource, - AssetCode -} from 'aws-cdk-lib/aws-appsync' -import { Construct } from 'constructs' -import * as path from 'path' -import { type ApiProps } from '.' + AssetCode, +} from 'aws-cdk-lib/aws-appsync'; import { Effect, Policy, PolicyDocument, PolicyStatement, Role, - ServicePrincipal -} from 'aws-cdk-lib/aws-iam' -import { fileURLToPath } from 'url' -import { dirname } from 'path' + ServicePrincipal, +} from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; +import { type ApiProps } from '.'; -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) interface ApiResolversProps extends ApiProps { - api: GraphqlApi + api: GraphqlApi; } export class ApiResolvers extends Construct { - constructor (scope: Construct, id: string, props: ApiResolversProps) { - super(scope, id) + constructor(scope: Construct, id: string, props: ApiResolversProps) { + super(scope, id); const { api, dataFeedTable, newsletterTable, unauthenticatedUserRole } = - props + props; - const functionsPath = path.join(__dirname, 'functions') + const functionsPath = path.join(__dirname, 'functions'); const getFunctionPath = ( functionName: string, - functionType: 'pipeline' | 'resolver' + functionType: 'pipeline' | 'resolver', ): string => { return path.join( functionsPath, 'out', functionType, functionName, - 'index.js' - ) - } + 'index.js', + ); + }; /** ****** DATA SOURCES FOR AppSync ******* **/ @@ -71,18 +67,18 @@ export class ApiResolvers extends Construct { 'dynamodb:DeleteItem', 'dynamodb:Query', 'dynamodb:Scan', - 'dynamodb:BatchGetItem' + 'dynamodb:BatchGetItem', ], resources: [ newsletterTable.tableArn, - `${newsletterTable.tableArn}/index/${props.newsletterTableItemTypeGSI}` - ] - }) - ] - }) - } - } - ) + `${newsletterTable.tableArn}/index/${props.newsletterTableItemTypeGSI}`, + ], + }), + ], + }), + }, + }, + ); const newsletterTableSource = new DynamoDbDataSource( this, @@ -92,9 +88,9 @@ export class ApiResolvers extends Construct { table: newsletterTable, serviceRole: newsletterTableSourceRole.withoutPolicyUpdates(), name: 'NewsletterTableSource', - description: 'DynamoDB data source for newsletter table' - } - ) + description: 'DynamoDB data source for newsletter table', + }, + ); const dataFeedTableSourceRole = new Role(this, 'DataFeedTableSourceRole', { assumedBy: new ServicePrincipal('appsync.amazonaws.com'), @@ -109,17 +105,17 @@ export class ApiResolvers extends Construct { 'dynamodb:DeleteItem', 'dynamodb:Query', 'dynamodb:Scan', - 'dynamodb:BatchGetItem' + 'dynamodb:BatchGetItem', ], resources: [ dataFeedTable.tableArn, - `${dataFeedTable.tableArn}/index/${props.dataFeedTableTypeIndex}` - ] - }) - ] - }) - } - }) + `${dataFeedTable.tableArn}/index/${props.dataFeedTableTypeIndex}`, + ], + }), + ], + }), + }, + }); const dataFeedTableSource = new DynamoDbDataSource( this, @@ -129,9 +125,9 @@ export class ApiResolvers extends Construct { table: dataFeedTable, description: 'DynamoDB data source for Data Feed table', serviceRole: dataFeedTableSourceRole.withoutPolicyUpdates(), - name: 'DataFeedTableSource' - } - ) + name: 'DataFeedTableSource', + }, + ); const dataFeedSubscriberLambdaSourceRole = new Role( this, @@ -143,13 +139,13 @@ export class ApiResolvers extends Construct { statements: [ new PolicyStatement({ actions: ['lambda:InvokeFunction'], - resources: [props.functions.feedSubscriberFunction.functionArn] - }) - ] - }) - } - } - ) + resources: [props.functions.feedSubscriberFunction.functionArn], + }), + ], + }), + }, + }, + ); const dataFeedSubscriberLambdaSource = new LambdaDataSource( this, @@ -159,9 +155,9 @@ export class ApiResolvers extends Construct { lambdaFunction: props.functions.feedSubscriberFunction, name: 'DataFeedSubscriberLambdaSource', description: 'Lambda data source for feedSubscriber function', - serviceRole: dataFeedSubscriberLambdaSourceRole.withoutPolicyUpdates() - } - ) + serviceRole: dataFeedSubscriberLambdaSourceRole.withoutPolicyUpdates(), + }, + ); const newsletterCreatorLambdaSourceRole = new Role( this, @@ -174,14 +170,14 @@ export class ApiResolvers extends Construct { new PolicyStatement({ actions: ['lambda:InvokeFunction'], resources: [ - props.functions.createNewsletterFunction.functionArn - ] - }) - ] - }) - } - } - ) + props.functions.createNewsletterFunction.functionArn, + ], + }), + ], + }), + }, + }, + ); const newsletterCreatorLambdaSource = new LambdaDataSource( this, @@ -191,9 +187,9 @@ export class ApiResolvers extends Construct { lambdaFunction: props.functions.createNewsletterFunction, name: 'NewsletterCreatorLambdaSource', description: 'Lambda data source for createNewsletter function', - serviceRole: newsletterCreatorLambdaSourceRole.withoutPolicyUpdates() - } - ) + serviceRole: newsletterCreatorLambdaSourceRole.withoutPolicyUpdates(), + }, + ); const userSubscriberLambdaSourceRole = new Role( this, @@ -205,13 +201,13 @@ export class ApiResolvers extends Construct { statements: [ new PolicyStatement({ actions: ['lambda:InvokeFunction'], - resources: [props.functions.userSubscriberFunction.functionArn] - }) - ] - }) - } - } - ) + resources: [props.functions.userSubscriberFunction.functionArn], + }), + ], + }), + }, + }, + ); const userSubscriberLambdaSource = new LambdaDataSource( this, @@ -221,9 +217,9 @@ export class ApiResolvers extends Construct { lambdaFunction: props.functions.userSubscriberFunction, name: 'UserSubscriberLambdaSource', description: 'Lambda data source for userSubscriber function', - serviceRole: userSubscriberLambdaSourceRole.withoutPolicyUpdates() - } - ) + serviceRole: userSubscriberLambdaSourceRole.withoutPolicyUpdates(), + }, + ); const userUnsubscriberLambdaSourceRole = new Role( this, @@ -236,14 +232,14 @@ export class ApiResolvers extends Construct { new PolicyStatement({ actions: ['lambda:InvokeFunction'], resources: [ - props.functions.userUnsubscriberFunction.functionArn - ] - }) - ] - }) - } - } - ) + props.functions.userUnsubscriberFunction.functionArn, + ], + }), + ], + }), + }, + }, + ); const userUnsubscriberLambdaSource = new LambdaDataSource( this, @@ -253,9 +249,9 @@ export class ApiResolvers extends Construct { lambdaFunction: props.functions.userUnsubscriberFunction, name: 'UserUnsubscriberLambdaSource', description: 'Lambda data source for userUnsubscriber function', - serviceRole: userUnsubscriberLambdaSourceRole.withoutPolicyUpdates() - } - ) + serviceRole: userUnsubscriberLambdaSourceRole.withoutPolicyUpdates(), + }, + ); const isAuthorizedFunctionSourceRole = new Role( this, @@ -268,14 +264,14 @@ export class ApiResolvers extends Construct { new PolicyStatement({ actions: ['lambda:InvokeFunction'], resources: [ - props.functions.graphqlReadAuthorizerFunction.functionArn - ] - }) - ] - }) - } - } - ) + props.functions.graphqlReadAuthorizerFunction.functionArn, + ], + }), + ], + }), + }, + }, + ); const isAuthorizedFunctionSource = new LambdaDataSource( this, @@ -285,9 +281,9 @@ export class ApiResolvers extends Construct { lambdaFunction: props.functions.graphqlReadAuthorizerFunction, name: 'isAuthorizedFunctionSource', description: 'Lambda data source for isAuthorized function', - serviceRole: isAuthorizedFunctionSourceRole.withoutPolicyUpdates() - } - ) + serviceRole: isAuthorizedFunctionSourceRole.withoutPolicyUpdates(), + }, + ); const filterListByAuthorizationFunctionSourceRole = new Role( this, @@ -301,14 +297,14 @@ export class ApiResolvers extends Construct { actions: ['lambda:InvokeFunction'], resources: [ props.functions.graphqlFilterReadAuthorizerFunction - .functionArn - ] - }) - ] - }) - } - } - ) + .functionArn, + ], + }), + ], + }), + }, + }, + ); const filterListByAuthorizationSource = new LambdaDataSource( this, @@ -319,9 +315,9 @@ export class ApiResolvers extends Construct { name: 'filterIsAuthorizedFunctionSource', description: 'Lambda data source for isAuthorized function', serviceRole: - filterListByAuthorizationFunctionSourceRole.withoutPolicyUpdates() - } - ) + filterListByAuthorizationFunctionSourceRole.withoutPolicyUpdates(), + }, + ); /** AppSync Resolver Pipeline Functions */ @@ -333,9 +329,9 @@ export class ApiResolvers extends Construct { dataSource: newsletterTableSource, name: 'getNewsletter', code: AssetCode.fromAsset(getFunctionPath('getNewsletter', 'pipeline')), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const listNewslettersOwned = new AppsyncFunction( this, @@ -345,11 +341,11 @@ export class ApiResolvers extends Construct { api, dataSource: newsletterTableSource, code: AssetCode.fromAsset( - getFunctionPath('listNewslettersOwned', 'pipeline') + getFunctionPath('listNewslettersOwned', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const listNewslettersDiscoverable = new AppsyncFunction( this, @@ -360,10 +356,10 @@ export class ApiResolvers extends Construct { dataSource: newsletterTableSource, runtime: FunctionRuntime.JS_1_0_0, code: AssetCode.fromAsset( - getFunctionPath('listNewslettersDiscoverable', 'pipeline') - ) - } - ) + getFunctionPath('listNewslettersDiscoverable', 'pipeline'), + ), + }, + ); const listNewslettersShared = new AppsyncFunction( this, @@ -374,10 +370,10 @@ export class ApiResolvers extends Construct { dataSource: newsletterTableSource, runtime: FunctionRuntime.JS_1_0_0, code: AssetCode.fromAsset( - getFunctionPath('listNewslettersShared', 'pipeline') - ) - } - ) + getFunctionPath('listNewslettersShared', 'pipeline'), + ), + }, + ); // const listNewslettersById = new AppsyncFunction( // this, @@ -401,11 +397,11 @@ export class ApiResolvers extends Construct { api, dataSource: newsletterTableSource, code: AssetCode.fromAsset( - getFunctionPath('listPublications', 'pipeline') + getFunctionPath('listPublications', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const getPublication = new AppsyncFunction( this, 'GetPublicationResolverFunction', @@ -414,11 +410,11 @@ export class ApiResolvers extends Construct { api, dataSource: newsletterTableSource, code: AssetCode.fromAsset( - getFunctionPath('getPublication', 'pipeline') + getFunctionPath('getPublication', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const updateNewsletterResolverFunction = new AppsyncFunction( this, @@ -428,11 +424,11 @@ export class ApiResolvers extends Construct { api, dataSource: newsletterTableSource, code: AssetCode.fromAsset( - getFunctionPath('updateNewsletter', 'pipeline') + getFunctionPath('updateNewsletter', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const subscribeToNewsletterFunction = new AppsyncFunction( this, 'SubscribeToNewsletterResolverFunction', @@ -441,11 +437,11 @@ export class ApiResolvers extends Construct { api, dataSource: userSubscriberLambdaSource, code: AssetCode.fromAsset( - getFunctionPath('subscribeToNewsletter', 'pipeline') + getFunctionPath('subscribeToNewsletter', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const unsubscribeFromNewsletter = new AppsyncFunction( this, @@ -455,11 +451,11 @@ export class ApiResolvers extends Construct { api, dataSource: userUnsubscriberLambdaSource, code: AssetCode.fromAsset( - getFunctionPath('unsubscribeFromNewsletter', 'pipeline') + getFunctionPath('unsubscribeFromNewsletter', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const listDataFeedsOwnedFunction = new AppsyncFunction( this, @@ -469,11 +465,11 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedTableSource, code: AssetCode.fromAsset( - getFunctionPath('listDataFeedsOwned', 'pipeline') + getFunctionPath('listDataFeedsOwned', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const listDataFeedsSharedFunction = new AppsyncFunction( this, @@ -483,11 +479,11 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedTableSource, code: AssetCode.fromAsset( - getFunctionPath('listDataFeedsShared', 'pipeline') + getFunctionPath('listDataFeedsShared', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const listDataFeedsDiscoverable = new AppsyncFunction( this, @@ -497,11 +493,11 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedTableSource, code: AssetCode.fromAsset( - getFunctionPath('listDataFeedsDiscoverable', 'pipeline') + getFunctionPath('listDataFeedsDiscoverable', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const createDataFeedFunction = new AppsyncFunction( this, @@ -511,11 +507,11 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedSubscriberLambdaSource, code: AssetCode.fromAsset( - getFunctionPath('createDataFeed', 'pipeline') + getFunctionPath('createDataFeed', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const getDataFeedFunction = new AppsyncFunction( this, 'GetDataFeedResolverFunction', @@ -524,9 +520,9 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedTableSource, code: AssetCode.fromAsset(getFunctionPath('getDataFeed', 'pipeline')), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const updateDataFeedFunction = new AppsyncFunction( this, @@ -536,11 +532,11 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedTableSource, code: AssetCode.fromAsset( - getFunctionPath('updateDataFeed', 'pipeline') + getFunctionPath('updateDataFeed', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const listArticlesFunction = new AppsyncFunction( this, @@ -550,9 +546,9 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedTableSource, code: AssetCode.fromAsset(getFunctionPath('listArticles', 'pipeline')), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const checkSubscriptionToNewsletterFunction = new AppsyncFunction( this, @@ -562,11 +558,11 @@ export class ApiResolvers extends Construct { api, dataSource: newsletterTableSource, code: AssetCode.fromAsset( - getFunctionPath('checkSubscriptionToNewsletter', 'pipeline') + getFunctionPath('checkSubscriptionToNewsletter', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const getNewsletterSubscriberStatsFunction = new AppsyncFunction( this, @@ -576,11 +572,11 @@ export class ApiResolvers extends Construct { api, dataSource: newsletterTableSource, code: AssetCode.fromAsset( - getFunctionPath('getNewsletterSubscriberStats', 'pipeline') + getFunctionPath('getNewsletterSubscriberStats', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const flagArticleFunction = new AppsyncFunction( this, @@ -590,9 +586,9 @@ export class ApiResolvers extends Construct { api, dataSource: dataFeedTableSource, code: AssetCode.fromAsset(getFunctionPath('flagArticle', 'pipeline')), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const listUserSubscriptionsFunction = new AppsyncFunction( this, @@ -602,19 +598,19 @@ export class ApiResolvers extends Construct { api, dataSource: newsletterTableSource, code: AssetCode.fromAsset( - getFunctionPath('listUserSubscriptions', 'pipeline') + getFunctionPath('listUserSubscriptions', 'pipeline'), ), - runtime: FunctionRuntime.JS_1_0_0 - } - ) + runtime: FunctionRuntime.JS_1_0_0, + }, + ); const isAuthorized = new AppsyncFunction(this, 'isAuthorized', { name: 'isAuthorized', api, dataSource: isAuthorizedFunctionSource, runtime: FunctionRuntime.JS_1_0_0, - code: AssetCode.fromAsset(getFunctionPath('isAuthorized', 'pipeline')) - }) + code: AssetCode.fromAsset(getFunctionPath('isAuthorized', 'pipeline')), + }); const filterListByAuthorization = new AppsyncFunction( this, @@ -625,10 +621,10 @@ export class ApiResolvers extends Construct { dataSource: filterListByAuthorizationSource, runtime: FunctionRuntime.JS_1_0_0, code: AssetCode.fromAsset( - getFunctionPath('filterListByAuthorization', 'pipeline') - ) - } - ) + getFunctionPath('filterListByAuthorization', 'pipeline'), + ), + }, + ); /** AppSync GraphQL API Resolvers */ @@ -638,8 +634,8 @@ export class ApiResolvers extends Construct { fieldName: 'getNewsletter', code: AssetCode.fromAsset(getFunctionPath('getNewsletter', 'resolver')), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [getNewsletterFunction, isAuthorized] - }) + pipelineConfig: [getNewsletterFunction, isAuthorized], + }); new Resolver(this, 'ListNewslettersResolver', { api, @@ -651,24 +647,24 @@ export class ApiResolvers extends Construct { listNewslettersOwned, listNewslettersDiscoverable, listNewslettersShared, - filterListByAuthorization - ] - }) + filterListByAuthorization, + ], + }); new Resolver(this, 'ListPublicationsResolver', { api, typeName: 'Query', fieldName: 'listPublications', code: AssetCode.fromAsset( - getFunctionPath('listPublications', 'resolver') + getFunctionPath('listPublications', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, pipelineConfig: [ getNewsletterFunction, listPublicationsFunction, - filterListByAuthorization - ] - }) + filterListByAuthorization, + ], + }); new Resolver(this, 'getPublicationResolverFunction', { api, @@ -676,23 +672,23 @@ export class ApiResolvers extends Construct { fieldName: 'getPublication', code: AssetCode.fromAsset(getFunctionPath('getPublication', 'resolver')), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [getPublication] - }) + pipelineConfig: [getPublication], + }); new Resolver(this, 'UpdateNewsletterResolver', { api, typeName: 'Mutation', fieldName: 'updateNewsletter', code: AssetCode.fromAsset( - getFunctionPath('updateNewsletter', 'resolver') + getFunctionPath('updateNewsletter', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, pipelineConfig: [ getNewsletterFunction, isAuthorized, - updateNewsletterResolverFunction - ] - }) + updateNewsletterResolverFunction, + ], + }); new Resolver(this, 'ListDataFeedsResolver', { api, @@ -704,9 +700,9 @@ export class ApiResolvers extends Construct { listDataFeedsOwnedFunction, listDataFeedsSharedFunction, listDataFeedsDiscoverable, - filterListByAuthorization - ] - }) + filterListByAuthorization, + ], + }); new Resolver(this, 'GetDataFeedResolver', { api, @@ -714,8 +710,8 @@ export class ApiResolvers extends Construct { fieldName: 'getDataFeed', code: AssetCode.fromAsset(getFunctionPath('getDataFeed', 'resolver')), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [getDataFeedFunction, isAuthorized] - }) + pipelineConfig: [getDataFeedFunction, isAuthorized], + }); new Resolver(this, 'UpdateDataFeedResolver', { api, @@ -726,9 +722,9 @@ export class ApiResolvers extends Construct { pipelineConfig: [ getDataFeedFunction, isAuthorized, - updateDataFeedFunction - ] - }) + updateDataFeedFunction, + ], + }); new Resolver(this, 'ListArticlesResolver', { api, @@ -736,8 +732,8 @@ export class ApiResolvers extends Construct { fieldName: 'listArticles', code: AssetCode.fromAsset(getFunctionPath('listArticles', 'resolver')), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [getDataFeedFunction, isAuthorized, listArticlesFunction] - }) + pipelineConfig: [getDataFeedFunction, isAuthorized, listArticlesFunction], + }); new Resolver(this, 'FlagArticleResolver', { api, @@ -745,12 +741,12 @@ export class ApiResolvers extends Construct { fieldName: 'flagArticle', code: AssetCode.fromAsset(getFunctionPath('flagArticle', 'resolver')), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [flagArticleFunction] - }) + pipelineConfig: [flagArticleFunction], + }); props.functions.feedSubscriberFunction.grantInvoke( - dataFeedSubscriberLambdaSource - ) + dataFeedSubscriberLambdaSource, + ); new Resolver(this, 'DataFeedSubscriberResolver', { api, @@ -758,8 +754,8 @@ export class ApiResolvers extends Construct { fieldName: 'createDataFeed', code: AssetCode.fromAsset(getFunctionPath('createDataFeed', 'resolver')), pipelineConfig: [createDataFeedFunction], - runtime: FunctionRuntime.JS_1_0_0 - }) + runtime: FunctionRuntime.JS_1_0_0, + }); new Resolver(this, 'CreateNewsletterResolver', { api, @@ -767,36 +763,36 @@ export class ApiResolvers extends Construct { typeName: 'Mutation', fieldName: 'createNewsletter', code: AssetCode.fromAsset( - getFunctionPath('createNewsletter', 'resolver') + getFunctionPath('createNewsletter', 'resolver'), ), - runtime: FunctionRuntime.JS_1_0_0 - }) + runtime: FunctionRuntime.JS_1_0_0, + }); new Resolver(this, 'UserSubscriberResolver', { api, typeName: 'Mutation', fieldName: 'subscribeToNewsletter', code: AssetCode.fromAsset( - getFunctionPath('subscribeToNewsletter', 'resolver') + getFunctionPath('subscribeToNewsletter', 'resolver'), ), pipelineConfig: [ getNewsletterFunction, isAuthorized, - subscribeToNewsletterFunction + subscribeToNewsletterFunction, ], - runtime: FunctionRuntime.JS_1_0_0 - }) + runtime: FunctionRuntime.JS_1_0_0, + }); new Resolver(this, 'UserUnsubscriberResolver', { api, typeName: 'Mutation', fieldName: 'unsubscribeFromNewsletter', code: AssetCode.fromAsset( - getFunctionPath('unsubscribeFromNewsletter', 'resolver') + getFunctionPath('unsubscribeFromNewsletter', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [unsubscribeFromNewsletter] - }) + pipelineConfig: [unsubscribeFromNewsletter], + }); const externalUnsubscribeResolver = new Resolver( this, @@ -806,85 +802,88 @@ export class ApiResolvers extends Construct { typeName: 'Mutation', fieldName: 'externalUnsubscribeFromNewsletter', code: AssetCode.fromAsset( - getFunctionPath('externalUnsubscribeFromNewsletter', 'resolver') + getFunctionPath('externalUnsubscribeFromNewsletter', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [unsubscribeFromNewsletter] - } - ) - unauthenticatedUserRole.attachInlinePolicy( - new Policy(this, 'UnauthRoleUnsubscribe', { - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: ['appsync:GraphQL'], - resources: [externalUnsubscribeResolver.arn] - }) - ] - }) - ) + pipelineConfig: [unsubscribeFromNewsletter], + }, + ); + if (unauthenticatedUserRole) { + unauthenticatedUserRole.attachInlinePolicy( + new Policy(this, 'UnauthRoleUnsubscribe', { + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['appsync:GraphQL'], + resources: [externalUnsubscribeResolver.arn], + }), + ], + }), + ); + } + new Resolver(this, 'CheckSubscriptionToNewsletterResolver', { api, typeName: 'Query', fieldName: 'checkSubscriptionToNewsletter', code: AssetCode.fromAsset( - getFunctionPath('checkSubscriptionToNewsletter', 'resolver') + getFunctionPath('checkSubscriptionToNewsletter', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, pipelineConfig: [ getNewsletterFunction, isAuthorized, - checkSubscriptionToNewsletterFunction - ] - }) + checkSubscriptionToNewsletterFunction, + ], + }); new Resolver(this, 'ListUserSubscriptionsResolver', { api, typeName: 'Query', fieldName: 'listUserSubscriptions', code: AssetCode.fromAsset( - getFunctionPath('listUserSubscriptions', 'resolver') + getFunctionPath('listUserSubscriptions', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [listUserSubscriptionsFunction, filterListByAuthorization] - }) + pipelineConfig: [listUserSubscriptionsFunction, filterListByAuthorization], + }); new Resolver(this, 'CanUpdateNewsletterResolver', { api, typeName: 'Query', fieldName: 'canUpdateNewsletter', code: AssetCode.fromAsset( - getFunctionPath('canUpdateNewsletter', 'resolver') + getFunctionPath('canUpdateNewsletter', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [getNewsletterFunction, isAuthorized] - }) + pipelineConfig: [getNewsletterFunction, isAuthorized], + }); new Resolver(this, 'CanUpdateDataFeedResolver', { api, typeName: 'Query', fieldName: 'canUpdateDataFeed', code: AssetCode.fromAsset( - getFunctionPath('canUpdateDataFeed', 'resolver') + getFunctionPath('canUpdateDataFeed', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, - pipelineConfig: [getDataFeedFunction, isAuthorized] - }) + pipelineConfig: [getDataFeedFunction, isAuthorized], + }); new Resolver(this, 'getNewsletterSubscriberStatsResolver', { api, typeName: 'Query', fieldName: 'getNewsletterSubscriberStats', code: AssetCode.fromAsset( - getFunctionPath('getNewsletterSubscriberStats', 'resolver') + getFunctionPath('getNewsletterSubscriberStats', 'resolver'), ), runtime: FunctionRuntime.JS_1_0_0, pipelineConfig: [ getNewsletterFunction, isAuthorized, - getNewsletterSubscriberStatsFunction - ] - }) + getNewsletterSubscriberStatsFunction, + ], + }); } } diff --git a/lib/authentication/index.pre-token-generation-hook.ts b/lib/authentication/index.pre-token-generation-hook.ts index be188cc..ae45404 100644 --- a/lib/authentication/index.pre-token-generation-hook.ts +++ b/lib/authentication/index.pre-token-generation-hook.ts @@ -4,93 +4,92 @@ * SPDX-License-Identifier: MIT-0 */ -import { Tracer } from '@aws-lambda-powertools/tracer' -import { Logger } from '@aws-lambda-powertools/logger' -import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware' -import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware' -import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics' -import { v4 as uuidv4 } from 'uuid' -import middy from '@middy/core' -import { type PreTokenGenerationAuthenticationTriggerEvent } from 'aws-lambda' +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; +import { + AdminUpdateUserAttributesCommand, + CognitoIdentityProviderClient, + type AdminUpdateUserAttributesCommandInput, +} from '@aws-sdk/client-cognito-identity-provider'; import { DynamoDBClient, type PutItemCommandInput, PutItemCommand, DynamoDBServiceException, type ScanCommandInput, - ScanCommand -} from '@aws-sdk/client-dynamodb' -import { marshall } from '@aws-sdk/util-dynamodb' -import { - AdminUpdateUserAttributesCommand, - CognitoIdentityProviderClient, - type AdminUpdateUserAttributesCommandInput -} from '@aws-sdk/client-cognito-identity-provider' + ScanCommand, +} from '@aws-sdk/client-dynamodb'; +import { marshall } from '@aws-sdk/util-dynamodb'; +import middy from '@middy/core'; +import { v4 as uuidv4 } from 'uuid'; -const SERVICE_NAME = 'post-authentication-hook' -const ACCOUNT_TABLE = process.env.ACCOUNT_TABLE +const SERVICE_NAME = 'post-authentication-hook'; +const ACCOUNT_TABLE = process.env.ACCOUNT_TABLE; -const tracer = new Tracer({ serviceName: SERVICE_NAME }) -const logger = new Logger({ serviceName: SERVICE_NAME }) -const metrics = new Metrics({ serviceName: SERVICE_NAME }) +const tracer = new Tracer({ serviceName: SERVICE_NAME }); +const logger = new Logger({ serviceName: SERVICE_NAME }); +const metrics = new Metrics({ serviceName: SERVICE_NAME }); -const dynamodb = tracer.captureAWSv3Client(new DynamoDBClient()) -const cognito = tracer.captureAWSv3Client(new CognitoIdentityProviderClient()) +const dynamodb = tracer.captureAWSv3Client(new DynamoDBClient()); +const cognito = tracer.captureAWSv3Client(new CognitoIdentityProviderClient()); const lambdaHandler = async ( - event: PreTokenGenerationAuthenticationTriggerEvent -): Promise => { - logger.debug('PostAuthenticationEventTriggered', { event }) - metrics.addMetric('PostAuthenticationEventTriggered', MetricUnits.Count, 1) - const { userAttributes } = event.request + event: any, +): Promise => { + logger.debug('PostAuthenticationEventTriggered', { event }); + metrics.addMetric('PostAuthenticationEventTriggered', MetricUnit.Count, 1); + const { userAttributes } = event.request; if ( userAttributes['custom:Account'] === undefined || userAttributes['custom:Account'] === null || userAttributes['custom:Account'].length < 1 ) { - logger.debug('No Account ID found for user! Creating a new one', { event }) - metrics.addMetric('NewAccountCreated', MetricUnits.Count, 1) - const userId = userAttributes.sub - let accountId = uuidv4() + logger.debug('No Account ID found for user! Creating a new one', { event }); + metrics.addMetric('NewAccountCreated', MetricUnit.Count, 1); + const userId = userAttributes.sub; + let accountId = uuidv4(); try { const input: PutItemCommandInput = { Item: marshall({ accountId, - userId + userId, }), ConditionExpression: 'attribute_not_exists(accountId)', - TableName: ACCOUNT_TABLE - } - const command = new PutItemCommand(input) - await dynamodb.send(command) + TableName: ACCOUNT_TABLE, + }; + const command = new PutItemCommand(input); + await dynamodb.send(command); event.response.claimsOverrideDetails.claimsToAddOrOverride = { - 'custom:Account': accountId - } - console.log(accountId) + 'custom:Account': accountId, + }; + console.log(accountId); } catch (error) { if (error instanceof DynamoDBServiceException) { if (error.name === 'ConditionalCheckFailedException') { - logger.debug('Account already exists for user', { event }) - metrics.addMetric('AccountAlreadyExists', MetricUnits.Count, 1) + logger.debug('Account already exists for user', { event }); + metrics.addMetric('AccountAlreadyExists', MetricUnit.Count, 1); const scanInput: ScanCommandInput = { TableName: ACCOUNT_TABLE, FilterExpression: 'userId = :userId', ExpressionAttributeValues: { - ':userId': { S: userId } - } - } - const command = new ScanCommand(scanInput) - const result = await dynamodb.send(command) + ':userId': { S: userId }, + }, + }; + const command = new ScanCommand(scanInput); + const result = await dynamodb.send(command); if ( result.Items === undefined || result.Items.length !== 1 || result.Items[0].accountId.S === undefined ) { throw new Error( - 'Account already exists for user but not found in database' - ) + 'Account already exists for user but not found in database', + ); } - accountId = result.Items[0].accountId.S + accountId = result.Items[0].accountId.S; } } } @@ -101,19 +100,19 @@ const lambdaHandler = async ( UserAttributes: [ { Name: 'custom:Account', - Value: accountId - } - ] - } - const command = new AdminUpdateUserAttributesCommand(input) - await cognito.send(command) + Value: accountId, + }, + ], + }; + const command = new AdminUpdateUserAttributesCommand(input); + await cognito.send(command); } catch (e) { - logger.error('Error updating user attributes', { e }) + logger.error('Error updating user attributes', { e }); } } - return event -} + return event; +}; export const handler = middy(lambdaHandler) .use(captureLambdaHandler(tracer)) - .use(injectLambdaContext(logger)) + .use(injectLambdaContext(logger)); diff --git a/lib/authentication/index.ts b/lib/authentication/index.ts index c547d17..235d881 100644 --- a/lib/authentication/index.ts +++ b/lib/authentication/index.ts @@ -3,21 +3,21 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Duration, RemovalPolicy, Stack, CfnOutput } from 'aws-cdk-lib' import { type IIdentityPool, IdentityPool, - UserPoolAuthenticationProvider -} from '@aws-cdk/aws-cognito-identitypool-alpha' + UserPoolAuthenticationProvider, +} from '@aws-cdk/aws-cognito-identitypool-alpha'; +import { Duration, RemovalPolicy, Stack, CfnOutput } from 'aws-cdk-lib'; import { type IUserPool, UserPool, UserPoolClient, StringAttribute, type IUserPoolClient, - ClientAttributes -} from 'aws-cdk-lib/aws-cognito' -import { Construct } from 'constructs' + ClientAttributes, +} from 'aws-cdk-lib/aws-cognito'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { Role, type IRole, @@ -25,88 +25,82 @@ import { Effect, Policy, ServicePrincipal, - ManagedPolicy -} from 'aws-cdk-lib/aws-iam' -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb' -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' + ManagedPolicy, +} from 'aws-cdk-lib/aws-iam'; import { ApplicationLogLevel, Architecture, LambdaInsightsVersion, LoggingFormat, Runtime, - Tracing -} from 'aws-cdk-lib/aws-lambda' -import { NagSuppressions } from 'cdk-nag' + Tracing, +} from 'aws-cdk-lib/aws-lambda'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; interface AuthenticationProps { - userPoolId?: string - userPoolArn?: string - userPoolClientId?: string + userPoolId?: string; + userPoolArn?: string; + userPoolClientId?: string; } export class Authentication extends Construct { - public readonly userPool: IUserPool - private readonly identityPool: IIdentityPool - private readonly authenticatedUserRole: IRole - public readonly unauthenticatedUserRole: IRole - private readonly userPoolClient: IUserPoolClient - public readonly userPoolId: string - public readonly userPoolArn: string - public readonly identityPoolId: string - public readonly authenticatedUserRoleArn: string - public readonly unauthenticatedUserRoleArn: string - public readonly userPoolClientId: string - public readonly accountTable: Table - public readonly accountTableUserIndex = 'userId-index' + public readonly userPool: IUserPool; + public readonly identityPool: IIdentityPool; + private readonly authenticatedUserRole?: IRole; + public readonly unauthenticatedUserRole?: IRole; + public readonly userPoolClient: IUserPoolClient; + public readonly userPoolId: string; + public readonly userPoolArn: string; + public readonly identityPoolId: string; + public readonly authenticatedUserRoleArn?: string; + public readonly unauthenticatedUserRoleArn?: string; + public readonly userPoolClientId: string; + public readonly accountTable: Table; + public readonly accountTableUserIndex = 'userId-index'; constructor (scope: Construct, id: string, props?: AuthenticationProps) { - super(scope, id) - const auth = this.node.tryGetContext('authConfig') + super(scope, id); + const auth = this.node.tryGetContext('authConfig'); const accountTable = new Table(this, 'AccountTable', { tableName: Stack.of(this).stackName + '-AccountTable', removalPolicy: RemovalPolicy.DESTROY, partitionKey: { name: 'accountId', - type: AttributeType.STRING + type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, - pointInTimeRecovery: true - }) + pointInTimeRecovery: true, + }); accountTable.addGlobalSecondaryIndex({ indexName: this.accountTableUserIndex, partitionKey: { name: 'userId', - type: AttributeType.STRING - } - }) - this.accountTable = accountTable + type: AttributeType.STRING, + }, + }); + this.accountTable = accountTable; const preTokenGenerationHookFunctionRole = new Role( this, 'pre-token-generation-hook-role', { - assumedBy: new ServicePrincipal('lambda.amazonaws.com') - } - ) + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + }, + ); preTokenGenerationHookFunctionRole.addManagedPolicy( ManagedPolicy.fromManagedPolicyArn( this, 'PreTokenGenRoleLambdaExecution', - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - ) - ) + 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ), + ); const preTokenGenerationHookFunction = new NodejsFunction( this, 'pre-token-generation-hook', { description: "Post Authentication, Pre-Token Generation Hook that creates a user's accountId", - entry: new URL( - import.meta.url.replace( - /(.*)(\..+)/, - '$1.' + 'pre-token-generation-hook' + '$2' - ) - ).pathname, handler: 'handler', role: preTokenGenerationHookFunctionRole, architecture: Architecture.ARM_64, @@ -119,47 +113,47 @@ export class Authentication extends Construct { timeout: Duration.seconds(5), environment: { POWERTOOLS_LOG_LEVEL: 'DEBUG', - ACCOUNT_TABLE: accountTable.tableName - } - } - ) + ACCOUNT_TABLE: accountTable.tableName, + }, + }, + ); if (auth === undefined || auth === null) { const selfSignUpEnabled = - this.node.tryGetContext('selfSignUpEnabled') ?? false + this.node.tryGetContext('selfSignUpEnabled') ?? false; const userPool = new UserPool(this, 'UserPool', { removalPolicy: RemovalPolicy.DESTROY, selfSignUpEnabled, signInAliases: { - email: true + email: true, }, standardAttributes: { email: { - required: true + required: true, }, givenName: { required: true, - mutable: true + mutable: true, }, familyName: { required: true, - mutable: true - } + mutable: true, + }, }, customAttributes: { - Account: new StringAttribute() + Account: new StringAttribute(), }, lambdaTriggers: { - postAuthentication: preTokenGenerationHookFunction - } - }) + postAuthentication: preTokenGenerationHookFunction, + }, + }); const clientWriteAttributes = new ClientAttributes().withStandardAttributes({ familyName: true, givenName: true, - email: true - }) + email: true, + }); const clientReadAttributes = - clientWriteAttributes.withCustomAttributes('Account') + clientWriteAttributes.withCustomAttributes('Account'); const userPoolClient = userPool.addClient('UserPoolClient', { generateSecret: false, readAttributes: clientReadAttributes, @@ -167,32 +161,32 @@ export class Authentication extends Construct { authFlows: { adminUserPassword: true, userPassword: true, - userSrp: true - } - }) - userPoolClient.node.addDependency(userPool) + userSrp: true, + }, + }); + userPoolClient.node.addDependency(userPool); const identityPool = new IdentityPool(this, 'IdentityPool', { authenticationProviders: { userPools: [ new UserPoolAuthenticationProvider({ userPool, - userPoolClient - }) - ] - } - }) - this.userPool = userPool - this.identityPool = identityPool - this.userPoolClient = userPoolClient - this.authenticatedUserRole = identityPool.authenticatedRole - this.unauthenticatedUserRole = identityPool.unauthenticatedRole + userPoolClient, + }), + ], + }, + }); + this.userPool = userPool; + this.identityPool = identityPool; + this.userPoolClient = userPoolClient; + this.authenticatedUserRole = identityPool.authenticatedRole; + this.unauthenticatedUserRole = identityPool.unauthenticatedRole; } else { const userPool = UserPool.fromUserPoolId( this, 'UserPool', - props?.userPoolId ?? (auth.cognito.userPoolId as string) - ) + props?.userPoolId ?? (auth.cognito.userPoolId as string), + ); if ( props?.userPoolClientId === undefined && auth.cognito.userPoolClientId === undefined @@ -202,30 +196,30 @@ export class Authentication extends Construct { authFlows: { adminUserPassword: true, userPassword: true, - userSrp: true - } - }) + userSrp: true, + }, + }); const identityPool = new IdentityPool(this, 'IdentityPool', { authenticationProviders: { userPools: [ new UserPoolAuthenticationProvider({ userPool, - userPoolClient - }) - ] - } - }) - this.userPoolClient = userPoolClient - this.identityPool = identityPool + userPoolClient, + }), + ], + }, + }); + this.userPoolClient = userPoolClient; + this.identityPool = identityPool; } else { const userPoolClientId = - props?.userPoolClientId ?? auth.cognito.userPoolClientId + props?.userPoolClientId ?? auth.cognito.userPoolClientId; this.userPoolClient = UserPoolClient.fromUserPoolClientId( this, 'UserPoolClient', - userPoolClientId as string - ) + userPoolClientId as string, + ); if ( auth.cognito.identityPoolId !== undefined && auth.cognito.authenticatedUserArn !== undefined @@ -233,42 +227,42 @@ export class Authentication extends Construct { this.identityPool = IdentityPool.fromIdentityPoolId( this, 'IdentityPool', - auth.cognito.identityPoolId as string - ) + auth.cognito.identityPoolId as string, + ); this.authenticatedUserRole = Role.fromRoleArn( this, 'AuthenticatedUserRole', auth.cognito.authenticatedUserArn as string, { - mutable: true - } - ) + mutable: true, + }, + ); this.unauthenticatedUserRole = Role.fromRoleArn( this, 'UnauthenticatedUserRole', auth.cognito.unauthenticatedUserArn as string, { - mutable: true - } - ) + mutable: true, + }, + ); } else { const identityPool = new IdentityPool(this, 'IdentityPool', { authenticationProviders: { userPools: [ new UserPoolAuthenticationProvider({ userPool, - userPoolClient: this.userPoolClient - }) - ] - } - }) - this.identityPool = identityPool - this.authenticatedUserRole = identityPool.authenticatedRole as Role + userPoolClient: this.userPoolClient, + }), + ], + }, + }); + this.identityPool = identityPool; + this.authenticatedUserRole = identityPool.authenticatedRole as Role; this.unauthenticatedUserRole = - identityPool.unauthenticatedRole as Role + identityPool.unauthenticatedRole as Role; } } - this.userPool = userPool + this.userPool = userPool; } preTokenGenerationHookFunctionRole.attachInlinePolicy( new Policy(this, 'pre-token-generation-hook-policy', { @@ -276,45 +270,45 @@ export class Authentication extends Construct { new PolicyStatement({ actions: ['dynamodb:PutItem', 'dynamodb:Scan'], resources: [accountTable.tableArn], - effect: Effect.ALLOW + effect: Effect.ALLOW, }), new PolicyStatement({ actions: ['cognito-idp:AdminUpdateUserAttributes'], - resources: [this.userPool.userPoolArn] - }) - ] - }) - ) + resources: [this.userPool.userPoolArn], + }), + ], + }), + ); preTokenGenerationHookFunctionRole.addManagedPolicy( - ManagedPolicy.fromAwsManagedPolicyName('AWSXrayWriteOnlyAccess') - ) + ManagedPolicy.fromAwsManagedPolicyName('AWSXrayWriteOnlyAccess'), + ); new CfnOutput(this, 'UserPoolLink', { - value: `https://${Stack.of(this).region}.console.aws.amazon.com/cognito/v2/idp/user-pools/${this.userPool.userPoolId}/users?region=${Stack.of(this).region}` - }) - this.userPoolId = this.userPool.userPoolId - this.userPoolArn = this.userPool.userPoolArn - this.identityPoolId = this.identityPool.identityPoolId - this.authenticatedUserRoleArn = this.authenticatedUserRole.roleArn - this.unauthenticatedUserRoleArn = this.unauthenticatedUserRole.roleArn - this.userPoolClientId = this.userPoolClient.userPoolClientId + value: `https://${Stack.of(this).region}.console.aws.amazon.com/cognito/v2/idp/user-pools/${this.userPool.userPoolId}/users?region=${Stack.of(this).region}`, + }); + this.userPoolId = this.userPool.userPoolId; + this.userPoolArn = this.userPool.userPoolArn; + this.identityPoolId = this.identityPool.identityPoolId; + this.authenticatedUserRoleArn = this.authenticatedUserRole?.roleArn; + this.unauthenticatedUserRoleArn = this.unauthenticatedUserRole?.roleArn; + this.userPoolClientId = this.userPoolClient.userPoolClientId; /** * Adding nag suppression to decrease sec requirements for login */ NagSuppressions.addResourceSuppressions(this.userPool, [ { id: 'AwsSolutions-COG1', - reason: "Skipping - Sample doesn't need advanced security" + reason: "Skipping - Sample doesn't need advanced security", }, { id: 'AwsSolutions-COG2', - reason: "Skipping - Sample doesn't need advanced security" + reason: "Skipping - Sample doesn't need advanced security", }, { id: 'AwsSolutions-COG3', - reason: "Skipping - Sample doesn't need advanced security" - } - ]) + reason: "Skipping - Sample doesn't need advanced security", + }, + ]); NagSuppressions.addResourceSuppressions( preTokenGenerationHookFunctionRole, @@ -322,10 +316,10 @@ export class Authentication extends Construct { { id: 'AwsSolutions-IAM5', reason: - 'Allowing PreTokenGenerationHookFunctionRole to have * policies' - } + 'Allowing PreTokenGenerationHookFunctionRole to have * policies', + }, ], - true - ) + true, + ); } } diff --git a/lib/authorization/authorization-helper.ts b/lib/authorization/authorization-helper.ts index 175528f..33bca19 100644 --- a/lib/authorization/authorization-helper.ts +++ b/lib/authorization/authorization-helper.ts @@ -4,202 +4,202 @@ * SPDX-License-Identifier: MIT-0 */ -import * as schemaMap from '../shared/api/types.json' -import { Kind, type OperationDefinitionNode, parse } from 'graphql' - +import { type Logger } from '@aws-lambda-powertools/logger'; import { type EntityItem, - type AttributeValue -} from '@aws-sdk/client-verifiedpermissions' -import { type Logger } from '@aws-lambda-powertools/logger' + type AttributeValue, +} from '@aws-sdk/client-verifiedpermissions'; +import { Kind, type OperationDefinitionNode, parse } from 'graphql'; +import * as schemaMap from '../shared/api/types.json'; + export const getEntityItem = ( schema: Record, entityId: string, entityType: string, entityData?: Record, - optionals?: { logger?: Logger } + optionals?: { logger?: Logger }, ): EntityItem => { - const { logger } = optionals ?? {} + const { logger } = optionals ?? {}; if (logger !== undefined) { - logger.debug(`getEntityItem: ${entityId} ${entityType}`) + logger.debug(`getEntityItem: ${entityId} ${entityType}`); } const item: EntityItem = { identifier: { entityType: `GenAINewsletter::${entityType}`, - entityId - } - } + entityId, + }, + }; if (entityData !== undefined) { - item.attributes = getEntityAttributes(schema, entityType, entityData) + item.attributes = getEntityAttributes(schema, entityType, entityData); } - return item -} + return item; +}; export const getEntityAttributes = ( schema: Record, entityType: string, - entityData: Record + entityData: Record, ): Record => { - const avpSchema = schema.GenAINewsletter - const entityAttributes: Record = {} + const avpSchema = schema.GenAINewsletter; + const entityAttributes: Record = {}; if (avpSchema !== undefined && avpSchema.entityTypes !== undefined) { - const entity = avpSchema.entityTypes[entityType] + const entity = avpSchema.entityTypes[entityType]; if (entity !== undefined && entity.shape !== undefined) { - const shape = entity.shape + const shape = entity.shape; if (shape !== undefined) { - const attributes = shape.attributes as Record + const attributes = shape.attributes as Record; if (attributes !== undefined) { Object.entries(attributes).forEach(([key, value]) => { - let entityDataForKey + let entityDataForKey; if (value.type === 'Entity') { Object.keys(entityData).forEach((dataKey) => { if ( entityData[dataKey].__typename !== undefined && entityData[dataKey].__typename === key ) { - entityDataForKey = entityData[dataKey] + entityDataForKey = entityData[dataKey]; } - }) + }); } else { - entityDataForKey = entityData[key] + entityDataForKey = entityData[key]; } if (entityDataForKey !== undefined && value.type !== undefined) { switch (value.type) { case 'Boolean': entityAttributes[key] = { - boolean: entityDataForKey - } - break + boolean: entityDataForKey, + }; + break; case 'Entity': entityAttributes[key] = { entityIdentifier: { entityId: entityDataForKey.id, - entityType: `GenAINewsletter::${key}` - } - } - break + entityType: `GenAINewsletter::${key}`, + }, + }; + break; case 'Long': entityAttributes[key] = { - long: entityDataForKey - } - break + long: entityDataForKey, + }; + break; case 'String': entityAttributes[key] = { - string: entityDataForKey - } - break + string: entityDataForKey, + }; + break; case 'Set': // entityAttributes[key] = { // set: [...entityData[key]] // } - break + break; case 'Record': - break + break; } } - }) + }); } } } } - return entityAttributes -} + return entityAttributes; +}; export const lowercaseFirstLetter = (stringVal: string): string => { - return stringVal.charAt(0).toLowerCase() + stringVal.slice(1) -} + return stringVal.charAt(0).toLowerCase() + stringVal.slice(1); +}; export const queryToActionAuth = (query: string): string => { - const ast = parse(query) + const ast = parse(query); const operationDefinition = ast.definitions.find((value) => { - return value.kind === Kind.OPERATION_DEFINITION - }) as OperationDefinitionNode + return value.kind === Kind.OPERATION_DEFINITION; + }) as OperationDefinitionNode; if (operationDefinition.selectionSet.kind === Kind.SELECTION_SET) { const queryFieldSelection = operationDefinition.selectionSet.selections.find((selection) => { - return selection.kind === Kind.FIELD - }) + return selection.kind === Kind.FIELD; + }); if ( queryFieldSelection !== undefined && queryFieldSelection !== null && queryFieldSelection.kind === Kind.FIELD ) { - return queryFieldSelection.name.value + return queryFieldSelection.name.value; } } - throw new Error('Unable to locate definition') -} + throw new Error('Unable to locate definition'); +}; export const queryToResourceEntity = (query: string): string => { - const action = queryToActionAuth(query) + const action = queryToActionAuth(query); const queries = schemaMap.__schema.types.find((type) => { - return type.name === 'Query' && type.kind === 'OBJECT' - }) + return type.name === 'Query' && type.kind === 'OBJECT'; + }); if (queries === undefined || queries === null) { - throw new Error('Unable to locate Query type') + throw new Error('Unable to locate Query type'); } const queryField = queries.fields?.find((field) => { - return field.name === action - }) + return field.name === action; + }); if (queryField?.type.name === null) { - return mutationToResourceEntity(query) + return mutationToResourceEntity(query); } if (queryField !== undefined) { - return queryField.type.name + return queryField.type.name; } else { - throw new Error('Unable to locate resource Entity') + throw new Error('Unable to locate resource Entity'); } -} +}; export const queryToResourcesEntity = (query: string): string => { - const action = queryToActionAuth(query) + const action = queryToActionAuth(query); const queries = schemaMap.__schema.types.find((type) => { - return type.name === 'Query' && type.kind === 'OBJECT' - }) + return type.name === 'Query' && type.kind === 'OBJECT'; + }); if (queries === undefined || queries === null) { - throw new Error('Unable to locate Query type') + throw new Error('Unable to locate Query type'); } const queryFieldType = queries.fields?.find((field) => { - return field.name === action - }) + return field.name === action; + }); if (queryFieldType !== undefined) { const queryFieldTypeObject = schemaMap.__schema.types.find((type) => { - return type.name === queryFieldType.type.name - }) + return type.name === queryFieldType.type.name; + }); const itemObject = queryFieldTypeObject?.fields?.find((fieldItem) => { - return fieldItem.name === 'items' - }) + return fieldItem.name === 'items'; + }); if ( itemObject !== undefined && itemObject.type.kind === 'LIST' && itemObject?.type?.ofType?.name !== undefined && itemObject?.type?.ofType?.name !== null ) { - return itemObject.type.ofType?.name + return itemObject.type.ofType?.name; } } - throw new Error('Unable to locate resource Entity') -} + throw new Error('Unable to locate resource Entity'); +}; export const mutationToResourceEntity = (query: string): string => { - const action = queryToActionAuth(query) + const action = queryToActionAuth(query); const queries = schemaMap.__schema.types.find((type) => { - return type.name === 'Mutation' && type.kind === 'OBJECT' - }) + return type.name === 'Mutation' && type.kind === 'OBJECT'; + }); if (queries === undefined || queries === null) { - throw new Error('Unable to locate Query type') + throw new Error('Unable to locate Query type'); } const queryField = queries.fields?.find((field) => { - return field.name === action - }) + return field.name === action; + }); if ( queryField === undefined || queryField === null || queryField.type.kind !== Kind.OBJECT || queryField.type.name === null ) { - throw new Error('Unable to locate action') + throw new Error('Unable to locate action'); } - return queryField.type.name -} + return queryField.type.name; +}; diff --git a/lib/authorization/index.action-authorization.ts b/lib/authorization/index.action-authorization.ts index a897e76..2e43873 100644 --- a/lib/authorization/index.action-authorization.ts +++ b/lib/authorization/index.action-authorization.ts @@ -3,184 +3,184 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Tracer } from '@aws-lambda-powertools/tracer' -import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware' -import { Logger } from '@aws-lambda-powertools/logger' -import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware' -import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics' -import { CognitoJwtVerifier } from 'aws-jwt-verify' +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; import { CognitoIdentityProviderClient, GetUserCommand, - type GetUserCommandInput -} from '@aws-sdk/client-cognito-identity-provider' // ES Modules import -import middy from '@middy/core' + type GetUserCommandInput, +} from '@aws-sdk/client-cognito-identity-provider'; // ES Modules import import { GetSchemaCommand, VerifiedPermissionsClient, type IsAuthorizedCommandInput, IsAuthorizedCommand, - Decision -} from '@aws-sdk/client-verifiedpermissions' -import { queryToActionAuth } from './authorization-helper' + Decision, +} from '@aws-sdk/client-verifiedpermissions'; +import middy from '@middy/core'; +import { CognitoJwtVerifier } from 'aws-jwt-verify'; +import { queryToActionAuth } from './authorization-helper'; -const SERVICE_NAME = 'authorization-check' +const SERVICE_NAME = 'authorization-check'; -const tracer = new Tracer({ serviceName: SERVICE_NAME }) -const logger = new Logger({ serviceName: SERVICE_NAME }) -const metrics = new Metrics({ serviceName: SERVICE_NAME }) +const tracer = new Tracer({ serviceName: SERVICE_NAME }); +const logger = new Logger({ serviceName: SERVICE_NAME }); +const metrics = new Metrics({ serviceName: SERVICE_NAME }); const { USER_POOL_ID, USER_POOL_CLIENT_ID, POLICY_STORE_ID, VALIDATION_REGEX } = - process.env + process.env; if (VALIDATION_REGEX === undefined || VALIDATION_REGEX === null) { - logger.error('VALIDATION_REGEX is not set') - throw new Error('VALIDATION_REGEX is not set') + logger.error('VALIDATION_REGEX is not set'); + throw new Error('VALIDATION_REGEX is not set'); } if (USER_POOL_ID === undefined || USER_POOL_ID === null) { - logger.error('USER_POOL_ID is not set') - throw new Error('USER_POOL_ID is not set') + logger.error('USER_POOL_ID is not set'); + throw new Error('USER_POOL_ID is not set'); } if (USER_POOL_CLIENT_ID === undefined || USER_POOL_CLIENT_ID === null) { - logger.error('USER_POOL_CLIENT_ID is not set') - throw new Error('USER_POOL_CLIENT_ID is not set') + logger.error('USER_POOL_CLIENT_ID is not set'); + throw new Error('USER_POOL_CLIENT_ID is not set'); } -const regex = new RegExp(VALIDATION_REGEX) +const regex = new RegExp(VALIDATION_REGEX); const jwtVerifier = CognitoJwtVerifier.create({ userPoolId: USER_POOL_ID, - tokenUse: 'access' -}) + tokenUse: 'access', +}); const verifiedpermissions = tracer.captureAWSv3Client( - new VerifiedPermissionsClient() -) + new VerifiedPermissionsClient(), +); const cognitoIdp = tracer.captureAWSv3Client( - new CognitoIdentityProviderClient() -) + new CognitoIdentityProviderClient(), +); -let schema: Record +let schema: Record; const lambdaHandler = async (event: any): Promise => { - logger.debug('AuthorizationCheckEventTriggered', { event }) + logger.debug('AuthorizationCheckEventTriggered', { event }); if ( schema === undefined || schema === null || Object.keys(schema).length === 0 ) { - logger.debug('AVP Schema not yet cached. Retrieving AVP Schema') + logger.debug('AVP Schema not yet cached. Retrieving AVP Schema'); const schemaResponse = await verifiedpermissions.send( - new GetSchemaCommand({ policyStoreId: POLICY_STORE_ID }) - ) + new GetSchemaCommand({ policyStoreId: POLICY_STORE_ID }), + ); if ( schemaResponse.schema !== undefined && schemaResponse.schema.length > 0 ) { - schema = JSON.parse(schemaResponse.schema) + schema = JSON.parse(schemaResponse.schema); } else { - metrics.addMetric('AuthCheckFailed', MetricUnits.Count, 1) - logger.error('Unable to locate AVP Schema. Unable to check auth') - throw Error('Unable to locate AVP Schema. Unable to check auth') + metrics.addMetric('AuthCheckFailed', MetricUnit.Count, 1); + logger.error('Unable to locate AVP Schema. Unable to check auth'); + throw Error('Unable to locate AVP Schema. Unable to check auth'); } } - const tokenMatch = event.authorizationToken.match(regex) + const tokenMatch = event.authorizationToken.match(regex); if (tokenMatch === undefined || tokenMatch === null) { return { - isAuthorized: false - } + isAuthorized: false, + }; } // token is prefixed with AUTH see https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-lambda-authorization-create-new-auth-token - const authorizationToken = tokenMatch[1] as string - logger.info(`authorizationToken: ${authorizationToken}`) + const authorizationToken = tokenMatch[1] as string; + logger.info(`authorizationToken: ${authorizationToken}`); const jwtPayload = await jwtVerifier.verify(authorizationToken, { clientId: USER_POOL_CLIENT_ID, - tokenUse: 'access' - }) - logger.info(`jwtPayload: ${JSON.stringify(jwtPayload)}`) - const accountId = await getUserAccountId(authorizationToken) + tokenUse: 'access', + }); + logger.info(`jwtPayload: ${JSON.stringify(jwtPayload)}`); + const accountId = await getUserAccountId(authorizationToken); const isAuthInput: IsAuthorizedCommandInput = { policyStoreId: POLICY_STORE_ID, principal: { entityId: jwtPayload.sub, - entityType: 'GenAINewsletter::User' + entityType: 'GenAINewsletter::User', }, action: { actionId: 'graphqlOperation', - actionType: 'GenAINewsletter::Action' + actionType: 'GenAINewsletter::Action', }, resource: { entityType: 'GenAINewsletter::Operation', entityId: lowercaseFirstLetter( - queryToActionAuth(event.requestContext.queryString as string) - ) + queryToActionAuth(event.requestContext.queryString as string), + ), }, entities: { entityList: [ { identifier: { entityType: 'GenAINewsletter::User', - entityId: jwtPayload.sub + entityId: jwtPayload.sub, }, attributes: { Account: { entityIdentifier: { entityType: 'GenAINewsletter::Account', - entityId: accountId - } - } - } + entityId: accountId, + }, + }, + }, }, { identifier: { entityType: 'GenAINewsletter::Operation', entityId: lowercaseFirstLetter( - queryToActionAuth(event.requestContext.queryString as string) - ) - } - } - ] - } - } - const command = new IsAuthorizedCommand(isAuthInput) - const response = await verifiedpermissions.send(command) + queryToActionAuth(event.requestContext.queryString as string), + ), + }, + }, + ], + }, + }; + const command = new IsAuthorizedCommand(isAuthInput); + const response = await verifiedpermissions.send(command); logger.debug('AVP REQUEST/RESPONSE', { request: isAuthInput, - response - }) + response, + }); if (response.decision === Decision.ALLOW.toString()) { - metrics.addMetric('AuthCheckPassed', MetricUnits.Count, 1) - logger.debug('Authorized') + metrics.addMetric('AuthCheckPassed', MetricUnit.Count, 1); + logger.debug('Authorized'); return { isAuthorized: true, resolverContext: { accountId, userId: jwtPayload.sub, - requestContext: JSON.stringify(event.requestContext) - } - } + requestContext: JSON.stringify(event.requestContext), + }, + }; } else { - metrics.addMetric('AuthCheckFailed', MetricUnits.Count, 1) - logger.debug('Not Authorized') + metrics.addMetric('AuthCheckFailed', MetricUnit.Count, 1); + logger.debug('Not Authorized'); return { isAuthorized: false, resolverContext: { accountId, userId: jwtPayload.sub, - requestContext: JSON.stringify(event.requestContext) - } - } + requestContext: JSON.stringify(event.requestContext), + }, + }; } -} +}; const getUserAccountId = async ( - authorizationToken: string + authorizationToken: string, ): Promise => { - logger.debug('getting accountId for authToken', { authorizationToken }) + logger.debug('getting accountId for authToken', { authorizationToken }); const input: GetUserCommandInput = { - AccessToken: authorizationToken - } - const command = new GetUserCommand(input) - const response = await cognitoIdp.send(command) + AccessToken: authorizationToken, + }; + const command = new GetUserCommand(input); + const response = await cognitoIdp.send(command); if ( response.UserAttributes !== undefined && response.UserAttributes.length > 0 @@ -190,18 +190,18 @@ const getUserAccountId = async ( attribute.Name === 'custom:Account' && attribute.Value !== undefined ) { - return attribute.Value + return attribute.Value; } } } - throw new Error('Unable to locate accountId in User Attributes') -} + throw new Error('Unable to locate accountId in User Attributes'); +}; const lowercaseFirstLetter = (stringVal: string): string => { - return stringVal.charAt(0).toLowerCase() + stringVal.slice(1) -} + return stringVal.charAt(0).toLowerCase() + stringVal.slice(1); +}; export const handler = middy() .handler(lambdaHandler) .use(captureLambdaHandler(tracer)) - .use(injectLambdaContext(logger)) + .use(injectLambdaContext(logger)); diff --git a/lib/authorization/index.list-filter-authorization.ts b/lib/authorization/index.list-filter-authorization.ts index f6cfc49..4bd21ce 100644 --- a/lib/authorization/index.list-filter-authorization.ts +++ b/lib/authorization/index.list-filter-authorization.ts @@ -3,74 +3,74 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Tracer } from '@aws-lambda-powertools/tracer' -import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware' -import { Logger } from '@aws-lambda-powertools/logger' -import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware' -import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics' +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; -import middy from '@middy/core' import { GetSchemaCommand, VerifiedPermissionsClient, type IsAuthorizedCommandInput, IsAuthorizedCommand, - Decision -} from '@aws-sdk/client-verifiedpermissions' + Decision, +} from '@aws-sdk/client-verifiedpermissions'; +import middy from '@middy/core'; import { getEntityItem, lowercaseFirstLetter, queryToActionAuth, - queryToResourcesEntity -} from './authorization-helper' + queryToResourcesEntity, +} from './authorization-helper'; -const SERVICE_NAME = 'list-filter-authorization' +const SERVICE_NAME = 'list-filter-authorization'; -const tracer = new Tracer({ serviceName: SERVICE_NAME }) -const logger = new Logger({ serviceName: SERVICE_NAME }) -const metrics = new Metrics({ serviceName: SERVICE_NAME }) +const tracer = new Tracer({ serviceName: SERVICE_NAME }); +const logger = new Logger({ serviceName: SERVICE_NAME }); +const metrics = new Metrics({ serviceName: SERVICE_NAME }); -const { POLICY_STORE_ID } = process.env +const { POLICY_STORE_ID } = process.env; if (POLICY_STORE_ID === undefined || POLICY_STORE_ID === null) { - logger.error('POLICY_STORE_ID is not set') - throw new Error('POLICY_STORE_ID is not set') + logger.error('POLICY_STORE_ID is not set'); + throw new Error('POLICY_STORE_ID is not set'); } const verifiedpermissions = tracer.captureAWSv3Client( - new VerifiedPermissionsClient() -) + new VerifiedPermissionsClient(), +); -let schema: Record +let schema: Record; const lambdaHandler = async (event: any): Promise => { - logger.debug('FilterAuthorizationCheckEventTriggered', { event }) - const { userId, accountId } = event + logger.debug('FilterAuthorizationCheckEventTriggered', { event }); + const { userId, accountId } = event; if ( schema === undefined || schema === null || Object.keys(schema).length === 0 ) { - logger.debug('AVP Schema not yet cached. Retrieving AVP Schema') + logger.debug('AVP Schema not yet cached. Retrieving AVP Schema'); const schemaResponse = await verifiedpermissions.send( - new GetSchemaCommand({ policyStoreId: POLICY_STORE_ID }) - ) + new GetSchemaCommand({ policyStoreId: POLICY_STORE_ID }), + ); if ( schemaResponse.schema !== undefined && schemaResponse.schema.length > 0 ) { - logger.debug('AVP Schema', { schema: schemaResponse.schema }) - schema = JSON.parse(schemaResponse.schema) + logger.debug('AVP Schema', { schema: schemaResponse.schema }); + schema = JSON.parse(schemaResponse.schema); } else { - metrics.addMetric('AuthCheckFailed', MetricUnits.Count, 1) - logger.error('Unable to locate AVP Schema. Unable to check auth') - throw Error('Unable to locate AVP Schema. Unable to check auth') + metrics.addMetric('AuthCheckFailed', MetricUnit.Count, 1); + logger.error('Unable to locate AVP Schema. Unable to check auth'); + throw Error('Unable to locate AVP Schema. Unable to check auth'); } } if (event.result.items !== undefined && event.result.items.length > 0) { logger.debug('Checking Item Authorization for Filtering', { - itemCount: event.result.items.length - }) - const unfilteredItemPromises: Array> = [] + itemCount: event.result.items.length, + }); + const unfilteredItemPromises: Array> = []; event.result.items.forEach(async (item: any) => { unfilteredItemPromises.push( checkItemAuthorization( @@ -78,118 +78,118 @@ const lambdaHandler = async (event: any): Promise => { schema, userId as string, accountId as string, - event.requestContext - ) - ) - }) - const resolvedAuthItems = await Promise.all(unfilteredItemPromises) + event.requestContext, + ), + ); + }); + const resolvedAuthItems = await Promise.all(unfilteredItemPromises); const items = resolvedAuthItems .filter((item) => item.authorization) - .map((item) => item.item) - logger.debug('Filtered Items', { itemCount: items.length }) + .map((item) => item.item); + logger.debug('Filtered Items', { itemCount: items.length }); if (items.length > 0) { return { isAuthorized: true, - items - } + items, + }; } else { return { isAuthorized: false, - items: [] - } + items: [], + }; } } else { return { isAuthorized: true, - items: [] - } + items: [], + }; } -} +}; const checkItemAuthorization = async ( item: any, - schema: Record, + schemaObj: Record, userId: string, accountId: string, - requestContext: any + requestContext: any, ): Promise<{ item: any; authorization: boolean }> => { - const queryString = requestContext.queryString as string + const queryString = requestContext.queryString as string; const isAuthInput: IsAuthorizedCommandInput = { policyStoreId: POLICY_STORE_ID, principal: { entityId: userId, - entityType: 'GenAINewsletter::User' + entityType: 'GenAINewsletter::User', }, action: { actionId: lowercaseFirstLetter(queryToActionAuth(queryString)), - actionType: 'GenAINewsletter::Action' + actionType: 'GenAINewsletter::Action', }, resource: { entityType: `GenAINewsletter::${queryToResourcesEntity(queryString)}`, - entityId: item.id + entityId: item.id, }, entities: { entityList: [ { identifier: { entityType: 'GenAINewsletter::User', - entityId: userId + entityId: userId, }, attributes: { Account: { entityIdentifier: { entityType: 'GenAINewsletter::Account', - entityId: accountId - } - } - } + entityId: accountId, + }, + }, + }, }, getEntityItem( - schema, + schemaObj, item.id as string, queryToResourcesEntity(queryString), item as Record, - { logger } - ) - ] - } - } + { logger }, + ), + ], + }, + }; logger.debug('AVP REQUEST', { - isAuthInput - }) + isAuthInput, + }); try { - const command = new IsAuthorizedCommand(isAuthInput) - const response = await verifiedpermissions.send(command) + const command = new IsAuthorizedCommand(isAuthInput); + const response = await verifiedpermissions.send(command); logger.debug('AVP RESPONSE', { - response - }) + response, + }); if (response.decision === Decision.ALLOW.toString()) { - metrics.addMetric('AuthCheckPassed', MetricUnits.Count, 1) - logger.debug('Authorized') + metrics.addMetric('AuthCheckPassed', MetricUnit.Count, 1); + logger.debug('Authorized'); return { item, - authorization: true - } + authorization: true, + }; } else { - metrics.addMetric('AuthCheckFailed', MetricUnits.Count, 1) - logger.debug('Not Authorized') + metrics.addMetric('AuthCheckFailed', MetricUnit.Count, 1); + logger.debug('Not Authorized'); return { item, - authorization: false - } + authorization: false, + }; } } catch (error) { - metrics.addMetric('AuthCheckFailed', MetricUnits.Count, 1) - logger.error('Error checking authorization', { error }) + metrics.addMetric('AuthCheckFailed', MetricUnit.Count, 1); + logger.error('Error checking authorization', { error }); } return { item, - authorization: false - } -} + authorization: false, + }; +}; export const handler = middy() .handler(lambdaHandler) .use(captureLambdaHandler(tracer)) - .use(injectLambdaContext(logger)) + .use(injectLambdaContext(logger)); diff --git a/lib/authorization/index.read-authorization.ts b/lib/authorization/index.read-authorization.ts index af9bfd2..e785d73 100644 --- a/lib/authorization/index.read-authorization.ts +++ b/lib/authorization/index.read-authorization.ts @@ -3,141 +3,141 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Tracer } from '@aws-lambda-powertools/tracer' -import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware' -import { Logger } from '@aws-lambda-powertools/logger' -import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware' -import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics' +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; // import { getEntityItem } from '../shared/api/schema-to-avp/permission-map' -import middy from '@middy/core' import { GetSchemaCommand, VerifiedPermissionsClient, type IsAuthorizedCommandInput, IsAuthorizedCommand, - Decision -} from '@aws-sdk/client-verifiedpermissions' + Decision, +} from '@aws-sdk/client-verifiedpermissions'; +import middy from '@middy/core'; import { getEntityItem, lowercaseFirstLetter, queryToActionAuth, - queryToResourceEntity -} from './authorization-helper' + queryToResourceEntity, +} from './authorization-helper'; -const SERVICE_NAME = 'read-authorization' +const SERVICE_NAME = 'read-authorization'; -const tracer = new Tracer({ serviceName: SERVICE_NAME }) -const logger = new Logger({ serviceName: SERVICE_NAME }) -const metrics = new Metrics({ serviceName: SERVICE_NAME }) +const tracer = new Tracer({ serviceName: SERVICE_NAME }); +const logger = new Logger({ serviceName: SERVICE_NAME }); +const metrics = new Metrics({ serviceName: SERVICE_NAME }); -const { POLICY_STORE_ID } = process.env +const { POLICY_STORE_ID } = process.env; if (POLICY_STORE_ID === undefined || POLICY_STORE_ID === null) { - logger.error('POLICY_STORE_ID is not set') - throw new Error('POLICY_STORE_ID is not set') + logger.error('POLICY_STORE_ID is not set'); + throw new Error('POLICY_STORE_ID is not set'); } const verifiedpermissions = tracer.captureAWSv3Client( - new VerifiedPermissionsClient() -) + new VerifiedPermissionsClient(), +); -let schema: Record +let schema: Record; const lambdaHandler = async (event: any): Promise => { - logger.debug('AuthorizationCheckEventTriggered', { event }) - const root = event.root as string | undefined - const contingentAction = event.contingentAction as string | undefined + logger.debug('AuthorizationCheckEventTriggered', { event }); + const root = event.root as string | undefined; + const contingentAction = event.contingentAction as string | undefined; if ( schema === undefined || schema === null || Object.keys(schema).length === 0 ) { - logger.debug('AVP Schema not yet cached. Retrieving AVP Schema') + logger.debug('AVP Schema not yet cached. Retrieving AVP Schema'); const schemaResponse = await verifiedpermissions.send( - new GetSchemaCommand({ policyStoreId: POLICY_STORE_ID }) - ) + new GetSchemaCommand({ policyStoreId: POLICY_STORE_ID }), + ); if ( schemaResponse.schema !== undefined && schemaResponse.schema.length > 0 ) { - logger.debug('AVP Schema', { schema: schemaResponse.schema }) - schema = JSON.parse(schemaResponse.schema) + logger.debug('AVP Schema', { schema: schemaResponse.schema }); + schema = JSON.parse(schemaResponse.schema); } else { - metrics.addMetric('AuthCheckFailed', MetricUnits.Count, 1) - logger.error('Unable to locate AVP Schema. Unable to check auth') - throw Error('Unable to locate AVP Schema. Unable to check auth') + metrics.addMetric('AuthCheckFailed', MetricUnit.Count, 1); + logger.error('Unable to locate AVP Schema. Unable to check auth'); + throw Error('Unable to locate AVP Schema. Unable to check auth'); } } - const queryString = event.requestContext.queryString as string + const queryString = event.requestContext.queryString as string; const isAuthInput: IsAuthorizedCommandInput = { policyStoreId: POLICY_STORE_ID, principal: { entityId: event.userId, - entityType: 'GenAINewsletter::User' + entityType: 'GenAINewsletter::User', }, action: { actionId: lowercaseFirstLetter( - contingentAction ?? queryToActionAuth(queryString) + contingentAction ?? queryToActionAuth(queryString), ), - actionType: 'GenAINewsletter::Action' + actionType: 'GenAINewsletter::Action', }, resource: { entityType: `GenAINewsletter::${root ?? queryToResourceEntity(queryString)}`, - entityId: event.result.id + entityId: event.result.id, }, entities: { entityList: [ { identifier: { entityType: 'GenAINewsletter::User', - entityId: event.userId + entityId: event.userId, }, attributes: { Account: { entityIdentifier: { entityType: 'GenAINewsletter::Account', - entityId: event.accountId - } - } - } + entityId: event.accountId, + }, + }, + }, }, getEntityItem( schema, event.result.id as string, root ?? queryToResourceEntity(queryString), event.result as Record, - { logger } - ) - ] - } - } + { logger }, + ), + ], + }, + }; logger.debug('AVP REQUEST', { - isAuthInput - }) - const command = new IsAuthorizedCommand(isAuthInput) - const response = await verifiedpermissions.send(command) + isAuthInput, + }); + const command = new IsAuthorizedCommand(isAuthInput); + const response = await verifiedpermissions.send(command); logger.debug('AVP RESPONSE', { - response - }) + response, + }); if (response.decision === Decision.ALLOW.toString()) { - metrics.addMetric('AuthCheckPassed', MetricUnits.Count, 1) - logger.debug('Authorized') + metrics.addMetric('AuthCheckPassed', MetricUnit.Count, 1); + logger.debug('Authorized'); return { isAuthorized: true, - returnResult: event.result - } + returnResult: event.result, + }; } else { - metrics.addMetric('AuthCheckFailed', MetricUnits.Count, 1) - logger.debug('Not Authorized') + metrics.addMetric('AuthCheckFailed', MetricUnit.Count, 1); + logger.debug('Not Authorized'); return { - isAuthorized: false - } + isAuthorized: false, + }; } -} +}; export const handler = middy() .handler(lambdaHandler) .use(captureLambdaHandler(tracer)) - .use(injectLambdaContext(logger)) + .use(injectLambdaContext(logger)); diff --git a/lib/authorization/index.ts b/lib/authorization/index.ts index 2ba043b..6aec100 100644 --- a/lib/authorization/index.ts +++ b/lib/authorization/index.ts @@ -3,58 +3,54 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ +import * as fs from 'fs'; +import * as path from 'path'; import { aws_verifiedpermissions as verifiedpermissions, Duration, - RemovalPolicy -} from 'aws-cdk-lib' -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' -import * as fs from 'fs' -import * as path from 'path' -import { Construct } from 'constructs' + RemovalPolicy, +} from 'aws-cdk-lib'; +import { + PolicyStatement, + Effect, + Policy, + PolicyDocument, +} from 'aws-cdk-lib/aws-iam'; import { ApplicationLogLevel, Architecture, LambdaInsightsVersion, LoggingFormat, Runtime, - Tracing -} from 'aws-cdk-lib/aws-lambda' -import { - PolicyStatement, - Effect, - Policy, - PolicyDocument -} from 'aws-cdk-lib/aws-iam' -import { type CfnPolicy } from 'aws-cdk-lib/aws-verifiedpermissions' -import { NagSuppressions } from 'cdk-nag' -import { fileURLToPath } from 'url' -import { dirname } from 'path' + Tracing, +} from 'aws-cdk-lib/aws-lambda'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { type CfnPolicy } from 'aws-cdk-lib/aws-verifiedpermissions'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) interface PermissionsProps { - userPoolId: string - userPoolClientId: string - userPoolArn: string + userPoolId: string; + userPoolClientId: string; + userPoolArn: string; } export class Authorization extends Construct { - public readonly policyStore: verifiedpermissions.CfnPolicyStore - public readonly graphqlActionAuthorizerFunction: NodejsFunction - public readonly graphqlReadAuthorizerFunction: NodejsFunction - public readonly graphqlFilterReadAuthorizerFunction: NodejsFunction - public readonly avpAuthorizerValidationRegex: string = '^Bearer AUTH(.*)' - readonly policyDefinitions: CfnPolicy[] + public readonly policyStore: verifiedpermissions.CfnPolicyStore; + public readonly graphqlActionAuthorizerFunction: NodejsFunction; + public readonly graphqlReadAuthorizerFunction: NodejsFunction; + public readonly graphqlFilterReadAuthorizerFunction: NodejsFunction; + public readonly avpAuthorizerValidationRegex: string = '^Bearer AUTH(.*)'; + readonly policyDefinitions: CfnPolicy[]; constructor (scope: Construct, id: string, props: PermissionsProps) { - const { userPoolId, userPoolClientId, userPoolArn } = props - super(scope, id) + const { userPoolId, userPoolClientId, userPoolArn } = props; + super(scope, id); const validationSettings: verifiedpermissions.CfnPolicyStore.ValidationSettingsProperty = { - mode: 'STRICT' - } + mode: 'STRICT', + }; const policyStore = new verifiedpermissions.CfnPolicyStore( this, @@ -63,12 +59,12 @@ export class Authorization extends Construct { schema: { cedarJson: fs .readFileSync(path.join(__dirname, 'cedarschema.json')) - .toString('utf-8') + .toString('utf-8'), }, - validationSettings - } - ) - this.policyStore = policyStore + validationSettings, + }, + ); + this.policyStore = policyStore; new verifiedpermissions.CfnIdentitySource(this, 'IdentitySource', { policyStoreId: policyStore.ref, @@ -76,32 +72,32 @@ export class Authorization extends Construct { configuration: { cognitoUserPoolConfiguration: { userPoolArn, - clientIds: [userPoolClientId] - } - } - }) + clientIds: [userPoolClientId], + }, + }, + }); - const policiesFolder = path.join(__dirname, 'policies') + const policiesFolder = path.join(__dirname, 'policies'); this.policyDefinitions = fs .readdirSync(policiesFolder) .filter((p) => { - const f = path.join(policiesFolder, p) - return fs.statSync(f).isFile() && p.endsWith('.cedar') + const f = path.join(policiesFolder, p); + return fs.statSync(f).isFile() && p.endsWith('.cedar'); }) .map((p) => { - const f = path.join(policiesFolder, p) + const f = path.join(policiesFolder, p); const policy = new verifiedpermissions.CfnPolicy(this, p, { policyStoreId: policyStore.ref, definition: { static: { description: p, - statement: fs.readFileSync(f).toString('utf-8') - } - } - }) - policy.applyRemovalPolicy(RemovalPolicy.DESTROY) - return policy - }) + statement: fs.readFileSync(f).toString('utf-8'), + }, + }, + }); + policy.applyRemovalPolicy(RemovalPolicy.DESTROY); + return policy; + }); const avpAccessPolicy = new Policy(this, 'AuthCheckAVPAccess', { document: new PolicyDocument({ @@ -109,14 +105,14 @@ export class Authorization extends Construct { new PolicyStatement({ actions: [ 'verifiedpermissions:IsAuthorized', - 'verifiedpermissions:GetSchema' + 'verifiedpermissions:GetSchema', ], resources: [policyStore.attrArn], - effect: Effect.ALLOW - }) - ] - }) - }) + effect: Effect.ALLOW, + }), + ], + }), + }); const graphqlActionAuthorizerFunction = new NodejsFunction( this, @@ -125,12 +121,6 @@ export class Authorization extends Construct { description: 'Function responsible for checking if requests are authorized to create items using Amazon Verified Permissions', handler: 'handler', - entry: new URL( - import.meta.url.replace( - /(.*)(\..+)/, - '$1.' + 'action-authorization' + '$2' - ) - ).pathname, architecture: Architecture.ARM_64, runtime: Runtime.NODEJS_20_X, tracing: Tracing.ACTIVE, @@ -143,12 +133,12 @@ export class Authorization extends Construct { USER_POOL_CLIENT_ID: userPoolClientId, USER_POOL_ID: userPoolId, POLICY_STORE_ID: policyStore.ref, - VALIDATION_REGEX: this.avpAuthorizerValidationRegex - } - } - ) + VALIDATION_REGEX: this.avpAuthorizerValidationRegex, + }, + }, + ); - graphqlActionAuthorizerFunction.role?.attachInlinePolicy(avpAccessPolicy) + graphqlActionAuthorizerFunction.role?.attachInlinePolicy(avpAccessPolicy); const graphqlReadAuthorizerFunction = new NodejsFunction( this, @@ -157,12 +147,6 @@ export class Authorization extends Construct { description: 'Function responsible for checking if requests are authorized to read/view data items using Amazon Verified Permissions', handler: 'handler', - entry: new URL( - import.meta.url.replace( - /(.*)(\..+)/, - '$1.' + 'read-authorization' + '$2' - ) - ).pathname, architecture: Architecture.ARM_64, runtime: Runtime.NODEJS_20_X, tracing: Tracing.ACTIVE, @@ -175,11 +159,11 @@ export class Authorization extends Construct { USER_POOL_CLIENT_ID: userPoolClientId, USER_POOL_ID: userPoolId, POLICY_STORE_ID: policyStore.ref, - VALIDATION_REGEX: this.avpAuthorizerValidationRegex - } - } - ) - graphqlReadAuthorizerFunction.role?.attachInlinePolicy(avpAccessPolicy) + VALIDATION_REGEX: this.avpAuthorizerValidationRegex, + }, + }, + ); + graphqlReadAuthorizerFunction.role?.attachInlinePolicy(avpAccessPolicy); const graphqlFilterReadAuthorizerFunction = new NodejsFunction( this, @@ -188,12 +172,6 @@ export class Authorization extends Construct { description: 'Function responsible for checking if requested resources are authorized for viewing data and filtering out unauthorized data from the list.', handler: 'handler', - entry: new URL( - import.meta.url.replace( - /(.*)(\..+)/, - '$1.' + 'list-filter-authorization' + '$2' - ) - ).pathname, architecture: Architecture.ARM_64, runtime: Runtime.NODEJS_20_X, tracing: Tracing.ACTIVE, @@ -205,33 +183,33 @@ export class Authorization extends Construct { POWERTOOLS_LOG_LEVEL: 'DEBUG', USER_POOL_CLIENT_ID: userPoolClientId, USER_POOL_ID: userPoolId, - POLICY_STORE_ID: policyStore.ref - } - } - ) + POLICY_STORE_ID: policyStore.ref, + }, + }, + ); graphqlFilterReadAuthorizerFunction.role?.attachInlinePolicy( - avpAccessPolicy - ) + avpAccessPolicy, + ); - this.graphqlActionAuthorizerFunction = graphqlActionAuthorizerFunction - this.graphqlReadAuthorizerFunction = graphqlReadAuthorizerFunction + this.graphqlActionAuthorizerFunction = graphqlActionAuthorizerFunction; + this.graphqlReadAuthorizerFunction = graphqlReadAuthorizerFunction; this.graphqlFilterReadAuthorizerFunction = - graphqlFilterReadAuthorizerFunction + graphqlFilterReadAuthorizerFunction; NagSuppressions.addResourceSuppressions( [ graphqlActionAuthorizerFunction, graphqlReadAuthorizerFunction, - graphqlFilterReadAuthorizerFunction + graphqlFilterReadAuthorizerFunction, ], [ { id: 'AwsSolutions-IAM5', reason: - 'The policy is restricted to the verifiedpermissions:IsAuthorized and verifiedpermissions:GetSchema actions' - } + 'The policy is restricted to the verifiedpermissions:IsAuthorized and verifiedpermissions:GetSchema actions', + }, ], - true - ) + true, + ); } } diff --git a/lib/cdk-nag-supressions.ts b/lib/cdk-nag-supressions.ts index 23578e0..f126e9f 100644 --- a/lib/cdk-nag-supressions.ts +++ b/lib/cdk-nag-supressions.ts @@ -1,5 +1,5 @@ -import { type Stack } from 'aws-cdk-lib' -import { NagSuppressions } from 'cdk-nag' +import { type Stack } from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; /** * Adds suppressions to the CDK Nag linter for the given stack. @@ -13,16 +13,16 @@ export const addNagSuppressions = (stack: Stack): void => { NagSuppressions.addStackSuppressions(stack, [ { id: 'AwsSolutions-IAM4', - reason: 'Allowing managed polices' + reason: 'Allowing managed polices', }, { id: 'AwsSolutions-IAM5', appliesTo: [ - 'Policy::arn::iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs' + 'Policy::arn::iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs', ], - reason: 'Allowing managed policy: AWSAppSyncPushToCloudWatchLogs' - } - ]) + reason: 'Allowing managed policy: AWSAppSyncPushToCloudWatchLogs', + }, + ]); /** * This is the Stack-Level Log Retention Custom Resource. * If cdk_nag throws an IAM5 error for LogRetention, confirm the logical ID hasn't changed. @@ -33,10 +33,10 @@ export const addNagSuppressions = (stack: Stack): void => { [ { id: 'AwsSolutions-IAM5', - reason: 'Allowing LogRetention to apply to any CloudWatch Resource' - } - ] - ) + reason: 'Allowing LogRetention to apply to any CloudWatch Resource', + }, + ], + ); /** * CDKBucketDeployment Resource must be referenced by Path * If cdk_nag throws errors for this resource, confirm the logical ID hasn't changed. @@ -47,10 +47,10 @@ export const addNagSuppressions = (stack: Stack): void => { [ { id: 'AwsSolutions-IAM5', - reason: 'Allowing CDKBucketDeployment to have * policies' - } - ] - ) + reason: 'Allowing CDKBucketDeployment to have * policies', + }, + ], + ); NagSuppressions.addResourceSuppressionsByPath( stack, `/${stack.stackName}/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource`, @@ -58,8 +58,8 @@ export const addNagSuppressions = (stack: Stack): void => { { id: 'AwsSolutions-L1', reason: - 'Allowing CDKBucketDeployment Lambda Runtime version to be managed by CDK version' - } - ] - ) -} + 'Allowing CDKBucketDeployment Lambda Runtime version to be managed by CDK version', + }, + ], + ); +}; diff --git a/lib/config.ts b/lib/config.ts index e23fd72..abee497 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -4,14 +4,9 @@ * SPDX-License-Identifier: MIT-0 */ -import path from 'path' -import { type DeployConfig } from '../lib/shared/common/deploy-config' -import { existsSync, readFileSync } from 'fs' -import { fileURLToPath } from 'url' -import { dirname } from 'path' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) +import { existsSync, readFileSync } from 'fs'; +import path from 'path'; +import { type DeployConfig } from '../lib/shared/common/deploy-config'; export default function getConfig (pathValue?: string): DeployConfig { if ( @@ -19,12 +14,12 @@ export default function getConfig (pathValue?: string): DeployConfig { ) { return JSON.parse( readFileSync( - pathValue ?? path.join(__dirname, '..', 'bin', 'config.json') - ).toString('utf8') - ) as DeployConfig + pathValue ?? path.join(__dirname, '..', 'bin', 'config.json'), + ).toString('utf8'), + ) as DeployConfig; } else { throw new Error( - 'Deploy config not found. Run `npm run config create` to configure the deployment.' - ) + 'Deploy config not found. Run `npm run config` to configure the deployment.', + ); } } diff --git a/lib/data-feed-ingestion/index.ts b/lib/data-feed-ingestion/index.ts index e1d48e5..ea809bb 100644 --- a/lib/data-feed-ingestion/index.ts +++ b/lib/data-feed-ingestion/index.ts @@ -3,61 +3,61 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import * as cdk from 'aws-cdk-lib' -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb' -import { Construct } from 'constructs' -import { type StateMachine } from 'aws-cdk-lib/aws-stepfunctions' -import { Stack } from 'aws-cdk-lib' -import { type NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' -import { type Bucket } from 'aws-cdk-lib/aws-s3' -import { RssAtomFeedConstruct } from './rss-atom-ingestion' +import * as cdk from 'aws-cdk-lib'; +import { Stack } from 'aws-cdk-lib'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { type NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { type Bucket } from 'aws-cdk-lib/aws-s3'; +import { type StateMachine } from 'aws-cdk-lib/aws-stepfunctions'; +import { Construct } from 'constructs'; +import { RssAtomFeedConstruct } from './rss-atom-ingestion'; export class NewsSubscriptionIngestion extends Construct { - public readonly dataFeedTable: Table - public readonly rssAtomDataBucket: Bucket - public readonly rssAtomIngestionStepFunctionStateMachine: StateMachine - public readonly dataFeedPollStepFunctionStateMachine: StateMachine - public readonly feedSubscriberFunction: NodejsFunction - public readonly dataFeedTableTypeIndex = 'type-index' - public readonly dataFeedTableLSI = 'lsi-date-index' + public readonly dataFeedTable: Table; + public readonly rssAtomDataBucket: Bucket; + public readonly rssAtomIngestionStepFunctionStateMachine: StateMachine; + public readonly dataFeedPollStepFunctionStateMachine: StateMachine; + public readonly feedSubscriberFunction: NodejsFunction; + public readonly dataFeedTableTypeIndex = 'type-index'; + public readonly dataFeedTableLSI = 'lsi-date-index'; constructor (scope: Construct, id: string, props: { loggingBucket: Bucket }) { - super(scope, id) - const { loggingBucket } = props + super(scope, id); + const { loggingBucket } = props; const dataFeedTable = new Table(this, 'DataFeedTable', { tableName: Stack.of(this).stackName + '-DataFeedTable', pointInTimeRecovery: true, removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE, partitionKey: { name: 'dataFeedId', - type: AttributeType.STRING + type: AttributeType.STRING, }, sortKey: { name: 'sk', - type: AttributeType.STRING + type: AttributeType.STRING, }, - billingMode: BillingMode.PAY_PER_REQUEST - }) + billingMode: BillingMode.PAY_PER_REQUEST, + }); dataFeedTable.addLocalSecondaryIndex({ indexName: this.dataFeedTableLSI, sortKey: { name: 'createdAt', - type: AttributeType.STRING - } - }) + type: AttributeType.STRING, + }, + }); dataFeedTable.addGlobalSecondaryIndex({ indexName: this.dataFeedTableTypeIndex, partitionKey: { name: 'sk', - type: AttributeType.STRING + type: AttributeType.STRING, }, sortKey: { name: 'accountId', - type: AttributeType.STRING - } - }) + type: AttributeType.STRING, + }, + }); const rssAtomIngestion = new RssAtomFeedConstruct( this, @@ -66,16 +66,16 @@ export class NewsSubscriptionIngestion extends Construct { dataFeedTable, dataFeedTableTypeIndex: this.dataFeedTableTypeIndex, dataFeedTableLSI: this.dataFeedTableLSI, - loggingBucket - } - ) + loggingBucket, + }, + ); - this.dataFeedTable = dataFeedTable - this.rssAtomDataBucket = rssAtomIngestion.rssAtomDataBucket - this.feedSubscriberFunction = rssAtomIngestion.feedSubscriberFunction + this.dataFeedTable = dataFeedTable; + this.rssAtomDataBucket = rssAtomIngestion.rssAtomDataBucket; + this.feedSubscriberFunction = rssAtomIngestion.feedSubscriberFunction; this.rssAtomIngestionStepFunctionStateMachine = - rssAtomIngestion.ingestionStepFunction.stateMachine + rssAtomIngestion.ingestionStepFunction.stateMachine; this.dataFeedPollStepFunctionStateMachine = - rssAtomIngestion.dataFeedPollStepFunction.stateMachine + rssAtomIngestion.dataFeedPollStepFunction.stateMachine; } } diff --git a/lib/data-feed-ingestion/prompts.ts b/lib/data-feed-ingestion/prompts.ts index deef6a3..9660080 100644 --- a/lib/data-feed-ingestion/prompts.ts +++ b/lib/data-feed-ingestion/prompts.ts @@ -5,33 +5,9 @@ */ export class ArticleIngestorPromptConfiguration { - private static readonly BASE_PROMPT = - '\n\nHuman: ' + - 'The following content is a news article. Read it carefully. You will need to use the information later. The article will be in
tags.\n' - - private static readonly USER_PROMPT_COMPONENT_BACKGROUND = - 'The next section of the prompt is provided by user input.\n' + - 'Use the user provided prompt to influence the article summarization. The user prompt will be inside tags.\n' - - private static readonly GENERATION_PROMPT = - 'Generate a summary of the article you read.\n' - - private static readonly USER_PROMPT_INCLUDED_PROMPT = - 'Use the provided user prompt as guidance for how you should summarize the information in the article' - - private static readonly PROMPT_RESTRICTIONS = - 'If you cannot access the article, respond with an error message inside tags and do not include any tags\n' + - 'Never include a preamble or any text prior to the summary.\n' + - 'If you do not know something, do not make it up.\n' + - 'If you are unable to read the content of the article, return null inside the tag\n' + - 'An error tag and a summary tag should not overlap, an error tag cannot be inside of a summary tag. \n' - - private static readonly PROMPT_RESPONSE_FORMAT_PROMPT = - 'Respond to the prompt using with the summary in tags:\n' - public static buildPrompt = ( articleBody: string, - summarizationPrompt?: string + summarizationPrompt?: string, ): string => { return ( this.BASE_PROMPT + @@ -50,6 +26,31 @@ export class ArticleIngestorPromptConfiguration { '\n' + this.PROMPT_RESPONSE_FORMAT_PROMPT + '\n\nAssistant:' - ) - } + ); + }; + + private static readonly BASE_PROMPT = + '\n\nHuman: ' + + 'The following content is a news article. Read it carefully. You will need to use the information later. The article will be in
tags.\n'; + + private static readonly USER_PROMPT_COMPONENT_BACKGROUND = + 'The next section of the prompt is provided by user input.\n' + + 'Use the user provided prompt to influence the article summarization. The user prompt will be inside tags.\n'; + + private static readonly GENERATION_PROMPT = + 'Generate a summary of the article you read.\n'; + + private static readonly USER_PROMPT_INCLUDED_PROMPT = + 'Use the provided user prompt as guidance for how you should summarize the information in the article'; + + private static readonly PROMPT_RESTRICTIONS = + 'If you cannot access the article, respond with an error message inside tags and do not include any tags\n' + + 'Never include a preamble or any text prior to the summary.\n' + + 'If you do not know something, do not make it up.\n' + + 'If you are unable to read the content of the article, return null inside the tag\n' + + 'An error tag and a summary tag should not overlap, an error tag cannot be inside of a summary tag. \n'; + + private static readonly PROMPT_RESPONSE_FORMAT_PROMPT = + 'Respond to the prompt using with the summary in tags:\n'; + } diff --git a/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.get-data-feeds.ts b/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.get-data-feeds.ts index 8ec5a14..b30ab62 100644 --- a/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.get-data-feeds.ts +++ b/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.get-data-feeds.ts @@ -3,36 +3,36 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Tracer } from '@aws-lambda-powertools/tracer' -import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware' -import { Logger } from '@aws-lambda-powertools/logger' -import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware' -import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics' -import middy from '@middy/core' +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; import { DynamoDBClient, QueryCommand, - type QueryCommandInput -} from '@aws-sdk/client-dynamodb' + type QueryCommandInput, +} from '@aws-sdk/client-dynamodb'; +import middy from '@middy/core'; -const SERVICE_NAME = 'get-data-feeds' +const SERVICE_NAME = 'get-data-feeds'; -const tracer = new Tracer({ serviceName: SERVICE_NAME }) -const logger = new Logger({ serviceName: SERVICE_NAME }) -const metrics = new Metrics({ serviceName: SERVICE_NAME }) +const tracer = new Tracer({ serviceName: SERVICE_NAME }); +const logger = new Logger({ serviceName: SERVICE_NAME }); +const metrics = new Metrics({ serviceName: SERVICE_NAME }); -const dynamodb = tracer.captureAWSv3Client(new DynamoDBClient()) +const dynamodb = tracer.captureAWSv3Client(new DynamoDBClient()); -const DATA_FEED_TABLE = process.env.DATA_FEED_TABLE -const DATA_FEED_TABLE_TYPE_INDEX = process.env.DATA_FEED_TABLE_TYPE_INDEX +const DATA_FEED_TABLE = process.env.DATA_FEED_TABLE; +const DATA_FEED_TABLE_TYPE_INDEX = process.env.DATA_FEED_TABLE_TYPE_INDEX; interface GetDataFeedsOutput { - dataFeeds: string[] - success: boolean + dataFeeds: string[]; + success: boolean; } const lambdaHandler = async (): Promise => { - logger.debug('Getting all data feeds') + logger.debug('Getting all data feeds'); try { const input: QueryCommandInput = { TableName: DATA_FEED_TABLE, @@ -41,41 +41,41 @@ const lambdaHandler = async (): Promise => { FilterExpression: '#enabled = :enabled', ExpressionAttributeNames: { '#type': 'sk', - '#enabled': 'enabled' + '#enabled': 'enabled', }, ExpressionAttributeValues: { ':type': { S: 'dataFeed' }, - ':enabled': { BOOL: true } - } - } - const command = new QueryCommand(input) - const response = await dynamodb.send(command) + ':enabled': { BOOL: true }, + }, + }; + const command = new QueryCommand(input); + const response = await dynamodb.send(command); if (response.Items === undefined) { - throw new Error('No data feeds found') + throw new Error('No data feeds found'); } - logger.debug('Data Feeds Found: ' + response.Items.length) - const dataFeeds: string[] = [] + logger.debug('Data Feeds Found: ' + response.Items.length); + const dataFeeds: string[] = []; for (const item of response.Items) { if (item.dataFeedId?.S !== undefined) { - metrics.addMetric('DataFeedsToPoll', MetricUnits.Count, 1) - dataFeeds.push(item.dataFeedId.S) + metrics.addMetric('DataFeedsToPoll', MetricUnit.Count, 1); + dataFeeds.push(item.dataFeedId.S); } } - logger.debug('Data Feeds: ' + dataFeeds.length) + logger.debug('Data Feeds: ' + dataFeeds.length); return { dataFeeds, - success: true - } + success: true, + }; } catch (error) { - logger.error('Error getting data feeds: ', { error }) + logger.error('Error getting data feeds: ', { error }); return { dataFeeds: [], - success: false - } + success: false, + }; } -} +}; export const handler = middy() .handler(lambdaHandler) .use(captureLambdaHandler(tracer)) - .use(injectLambdaContext(logger)) + .use(injectLambdaContext(logger)); diff --git a/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.ts b/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.ts index 6485960..5d9bd97 100644 --- a/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.ts +++ b/lib/data-feed-ingestion/rss-atom-ingestion/data-feed-poll-step-function.ts @@ -3,58 +3,55 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import * as cdk from 'aws-cdk-lib' -import { RemovalPolicy, Stack, type StackProps } from 'aws-cdk-lib' -import { type Table } from 'aws-cdk-lib/aws-dynamodb' -import { Rule, Schedule } from 'aws-cdk-lib/aws-events' +import * as cdk from 'aws-cdk-lib'; +import { RemovalPolicy, Stack, type StackProps } from 'aws-cdk-lib'; +import { type Table } from 'aws-cdk-lib/aws-dynamodb'; +import { Rule, Schedule } from 'aws-cdk-lib/aws-events'; +import { SfnStateMachine as StateMachineTarget } from 'aws-cdk-lib/aws-events-targets'; import { ApplicationLogLevel, Architecture, LambdaInsightsVersion, LoggingFormat, Runtime, - Tracing -} from 'aws-cdk-lib/aws-lambda' -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' -import { LogGroup } from 'aws-cdk-lib/aws-logs' + Tracing, +} from 'aws-cdk-lib/aws-lambda'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { LogGroup } from 'aws-cdk-lib/aws-logs'; import { StateMachine, IntegrationPattern, Map, DefinitionBody, JsonPath, - LogLevel -} from 'aws-cdk-lib/aws-stepfunctions' + LogLevel, +} from 'aws-cdk-lib/aws-stepfunctions'; import { LambdaInvoke, - StepFunctionsStartExecution -} from 'aws-cdk-lib/aws-stepfunctions-tasks' -import { SfnStateMachine as StateMachineTarget } from 'aws-cdk-lib/aws-events-targets' -import { Construct } from 'constructs' -import { NagSuppressions } from 'cdk-nag' + StepFunctionsStartExecution, +} from 'aws-cdk-lib/aws-stepfunctions-tasks'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; interface DataFeedPollStepFunctionProps extends StackProps { - dataFeedTable: Table - dataFeedIngestionStateMachine: StateMachine - dataFeedTableTypeIndex: string + dataFeedTable: Table; + dataFeedIngestionStateMachine: StateMachine; + dataFeedTableTypeIndex: string; } export class DataFeedPollStepFunction extends Construct { - public readonly stateMachine: StateMachine + public readonly stateMachine: StateMachine; constructor ( scope: Construct, id: string, - props: DataFeedPollStepFunctionProps + props: DataFeedPollStepFunctionProps, ) { - super(scope, id) + super(scope, id); const getDataFeedsFunction = new NodejsFunction(this, 'get-data-feeds', { description: 'Function responsible for getting all enabled data feeds to poll', handler: 'handler', - entry: new URL( - import.meta.url.replace(/(.*)(\..+)/, '$1.' + 'get-data-feeds' + '$2') - ).pathname, architecture: Architecture.ARM_64, runtime: Runtime.NODEJS_20_X, tracing: Tracing.ACTIVE, @@ -64,38 +61,38 @@ export class DataFeedPollStepFunction extends Construct { environment: { POWERTOOLS_LOG_LEVEL: 'DEBUG', DATA_FEED_TABLE: props.dataFeedTable.tableName, - DATA_FEED_TABLE_TYPE_INDEX: props.dataFeedTableTypeIndex + DATA_FEED_TABLE_TYPE_INDEX: props.dataFeedTableTypeIndex, }, - timeout: cdk.Duration.seconds(30) - }) + timeout: cdk.Duration.seconds(30), + }); // Step Function Tasks // Get Data Feeds from Data Feed Table const getDataFeedsJob = new LambdaInvoke(this, 'GetDataFeeds', { lambdaFunction: getDataFeedsFunction, payloadResponseOnly: true, - resultPath: '$' - }) + resultPath: '$', + }); const startIngestionStepFunctionJob = new StepFunctionsStartExecution( this, 'StartIngestionStepFunction', { stateMachine: props.dataFeedIngestionStateMachine, - integrationPattern: IntegrationPattern.REQUEST_RESPONSE - } - ) + integrationPattern: IntegrationPattern.REQUEST_RESPONSE, + }, + ); const mapDataFeeds = new Map(this, 'MapDataFeeds', { itemsPath: '$.dataFeeds', itemSelector: { - dataFeedId: JsonPath.stringAt('$$.Map.Item.Value') - } - }) + dataFeedId: JsonPath.stringAt('$$.Map.Item.Value'), + }, + }); - mapDataFeeds.itemProcessor(startIngestionStepFunctionJob) + mapDataFeeds.itemProcessor(startIngestionStepFunctionJob); - const definition = getDataFeedsJob.next(mapDataFeeds) + const definition = getDataFeedsJob.next(mapDataFeeds); const stateMachine = new StateMachine(this, 'StateMachine', { comment: @@ -104,22 +101,22 @@ export class DataFeedPollStepFunction extends Construct { logs: { destination: new LogGroup(this, 'DataFeedPollStepFunction', { logGroupName: `/aws/vendedlogs/states/${Stack.of(this).stackName}-data-feed-poll-step-function`, - removalPolicy: RemovalPolicy.DESTROY + removalPolicy: RemovalPolicy.DESTROY, }), level: LogLevel.ALL, - includeExecutionData: true + includeExecutionData: true, }, - tracingEnabled: true - }) - getDataFeedsFunction.grantInvoke(stateMachine) - props.dataFeedTable.grantReadData(getDataFeedsFunction) + tracingEnabled: true, + }); + getDataFeedsFunction.grantInvoke(stateMachine); + props.dataFeedTable.grantReadData(getDataFeedsFunction); new Rule(this, 'DataFeedCheckRule', { schedule: Schedule.rate(cdk.Duration.days(1)), - targets: [new StateMachineTarget(stateMachine)] - }) + targets: [new StateMachineTarget(stateMachine)], + }); - this.stateMachine = stateMachine + this.stateMachine = stateMachine; /** * CDK NAG Suppressions @@ -129,10 +126,10 @@ export class DataFeedPollStepFunction extends Construct { [ { id: 'AwsSolutions-IAM5', - reason: 'Allowing CloudWatch & XRay' - } + reason: 'Allowing CloudWatch & XRay', + }, ], - true - ) + true, + ); } } diff --git a/lib/data-feed-ingestion/rss-atom-ingestion/index.feed-subscriber.ts b/lib/data-feed-ingestion/rss-atom-ingestion/index.feed-subscriber.ts index 728cb07..d61550a 100644 --- a/lib/data-feed-ingestion/rss-atom-ingestion/index.feed-subscriber.ts +++ b/lib/data-feed-ingestion/rss-atom-ingestion/index.feed-subscriber.ts @@ -3,84 +3,84 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Tracer } from '@aws-lambda-powertools/tracer' -import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware' -import { Logger } from '@aws-lambda-powertools/logger' -import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware' -import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics' -import axios from 'axios' -import middy from '@middy/core' -import * as cheerio from 'cheerio' -import { v4 as uuidv4 } from 'uuid' +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; import { DynamoDBClient, PutItemCommand, - type PutItemInput -} from '@aws-sdk/client-dynamodb' + type PutItemInput, +} from '@aws-sdk/client-dynamodb'; import { SFNClient, StartExecutionCommand, - type StartExecutionCommandInput -} from '@aws-sdk/client-sfn' -import { marshall } from '@aws-sdk/util-dynamodb' + type StartExecutionCommandInput, +} from '@aws-sdk/client-sfn'; +import { marshall } from '@aws-sdk/util-dynamodb'; +import middy from '@middy/core'; +import axios from 'axios'; +import * as cheerio from 'cheerio'; +import { v4 as uuidv4 } from 'uuid'; import { DataFeedType, type CreateDataFeedInput, - type DataFeed -} from '../../shared/api/API' + type DataFeed, +} from '../../shared/api/API'; -const SERVICE_NAME = 'feed-subscriber' +const SERVICE_NAME = 'feed-subscriber'; -const tracer = new Tracer({ serviceName: SERVICE_NAME }) -const logger = new Logger({ serviceName: SERVICE_NAME }) -const metrics = new Metrics({ serviceName: SERVICE_NAME }) +const tracer = new Tracer({ serviceName: SERVICE_NAME }); +const logger = new Logger({ serviceName: SERVICE_NAME }); +const metrics = new Metrics({ serviceName: SERVICE_NAME }); -const dynamodb = tracer.captureAWSv3Client(new DynamoDBClient()) +const dynamodb = tracer.captureAWSv3Client(new DynamoDBClient()); -const DATA_FEED_TABLE = process.env.DATA_FEED_TABLE -const INGESTION_STEP_FUNCTION = process.env.INGESTION_STEP_FUNCTION +const DATA_FEED_TABLE = process.env.DATA_FEED_TABLE; +const INGESTION_STEP_FUNCTION = process.env.INGESTION_STEP_FUNCTION; const lambdaHander = async (event: { - accountId: string - input: CreateDataFeedInput + accountId: string; + input: CreateDataFeedInput; }): Promise => { - logger.debug('Starting Feed Subscriber, input: ' + JSON.stringify(event)) - metrics.addMetric('SubscriberInvocations', MetricUnits.Count, 1) - const { url, summarizationPrompt, title, description, enabled } = event.input - const isPrivate: boolean = event.input.isPrivate ?? true + logger.debug('Starting Feed Subscriber, input: ' + JSON.stringify(event)); + metrics.addMetric('SubscriberInvocations', MetricUnit.Count, 1); + const { url, summarizationPrompt, title, description, enabled } = event.input; + const isPrivate: boolean = event.input.isPrivate ?? true; if (url === undefined) { - throw new Error('URL is required') + throw new Error('URL is required'); } - const { accountId } = event + const { accountId } = event; try { - const response = await axios.get(url) - const $ = cheerio.load(response.data as string, { xmlMode: true }) + const response = await axios.get(url); + const $ = cheerio.load(response.data as string, { xmlMode: true }); - const dataFeedId = uuidv4() - let feedType: DataFeedType + const dataFeedId = uuidv4(); + let feedType: DataFeedType; if ($('rss').length > 0 && $('rss').attr('version') === '2.0') { - metrics.addMetric('RSSFeeds', MetricUnits.Count, 1) - logger.debug('Found RSS feed') - feedType = DataFeedType.RSS + metrics.addMetric('RSSFeeds', MetricUnit.Count, 1); + logger.debug('Found RSS feed'); + feedType = DataFeedType.RSS; } else if ( $('feed').length > 0 && $('feed').attr('xmlns') === 'http://www.w3.org/2005/Atom' ) { - metrics.addMetric('ATOMFeeds', MetricUnits.Count, 1) - logger.debug('Found ATOM feed') - feedType = DataFeedType.ATOM + metrics.addMetric('ATOMFeeds', MetricUnit.Count, 1); + logger.debug('Found ATOM feed'); + feedType = DataFeedType.ATOM; } else { - metrics.addMetric('InvalidFeedFormat', MetricUnits.Count, 1) + metrics.addMetric('InvalidFeedFormat', MetricUnit.Count, 1); throw Error( - 'Unknown feed format. The URL provided must be a URL to a RSS feed or ATOM feed.' - ) + 'Unknown feed format. The URL provided must be a URL to a RSS feed or ATOM feed.', + ); } const dataFeed: DataFeed = { feedType, title, account: { __typename: 'Account', - id: accountId + id: accountId, }, url, enabled, @@ -88,19 +88,19 @@ const lambdaHander = async (event: { summarizationPrompt, isPrivate, __typename: 'DataFeed', - id: dataFeedId - } - await storeDataFeed(dataFeed) - await startIngestionStepFunction(dataFeedId) - return dataFeed + id: dataFeedId, + }; + await storeDataFeed(dataFeed); + await startIngestionStepFunction(dataFeedId); + return dataFeed; } catch (error) { logger.error('There was an error subscribing to the provided URL', { - data: error - }) - tracer.addErrorAsMetadata(error as Error) - throw error + data: error, + }); + tracer.addErrorAsMetadata(error as Error); + throw error; } -} +}; const storeDataFeed = async (dataFeed: DataFeed): Promise => { const { @@ -111,9 +111,9 @@ const storeDataFeed = async (dataFeed: DataFeed): Promise => { title, description, account: { id: accountId }, - isPrivate - } = dataFeed - logger.debug('Storing data feed', { dataFeed }) + isPrivate, + } = dataFeed; + logger.debug('Storing data feed', { dataFeed }); const input: PutItemInput = { TableName: DATA_FEED_TABLE, Item: marshall( @@ -128,43 +128,42 @@ const storeDataFeed = async (dataFeed: DataFeed): Promise => { summarizationPrompt, title, description, - isPrivate + isPrivate, }, - { removeUndefinedValues: true } - ) - } - const command = new PutItemCommand(input) - const response = await dynamodb.send(command) + { removeUndefinedValues: true }, + ), + }; + const command = new PutItemCommand(input); + const response = await dynamodb.send(command); if (response.$metadata.httpStatusCode !== 200) { - tracer.putAnnotation('error', 'Error storing datafeed data') - tracer.putMetadata('FailedDDBPut', command) - metrics.addMetric('DDBPutFailed', MetricUnits.Count, 1) - throw Error('Error storing datafeed data') + tracer.putAnnotation('error', 'Error storing datafeed data'); + tracer.putMetadata('FailedDDBPut', command); + metrics.addMetric('DDBPutFailed', MetricUnit.Count, 1); + throw Error('Error storing datafeed data'); } else { - metrics.addMetric('DDBPutSuccessful', MetricUnits.Count, 1) + metrics.addMetric('DDBPutSuccessful', MetricUnit.Count, 1); } -} +}; const startIngestionStepFunction = async ( - dataFeedId: string + dataFeedId: string, ): Promise => { const input: StartExecutionCommandInput = { stateMachineArn: INGESTION_STEP_FUNCTION, - input: JSON.stringify({ dataFeedId }) - } - const command = new StartExecutionCommand(input) - const response = await new SFNClient().send(command) + input: JSON.stringify({ dataFeedId }), + }; + const command = new StartExecutionCommand(input); + const response = await new SFNClient().send(command); if (response.$metadata.httpStatusCode !== 200) { - tracer.putAnnotation('error', 'Error starting ingestion step function') - tracer.putMetadata('FailedSFNStart', command) - metrics.addMetric('SFNStartFailed', MetricUnits.Count, 1) - throw Error('Error starting ingestion step function') + tracer.putAnnotation('error', 'Error starting ingestion step function'); + tracer.putMetadata('FailedSFNStart', command); + metrics.addMetric('SFNStartFailed', MetricUnit.Count, 1); + throw Error('Error starting ingestion step function'); } else { - metrics.addMetric('SFNStartSuccessful', MetricUnits.Count, 1) + metrics.addMetric('SFNStartSuccessful', MetricUnit.Count, 1); } -} +}; -export const handler = middy() - .handler(lambdaHander) +export const handler = middy(lambdaHander) .use(captureLambdaHandler(tracer)) - .use(injectLambdaContext(logger)) + .use(injectLambdaContext(logger)); diff --git a/lib/data-feed-ingestion/rss-atom-ingestion/index.ts b/lib/data-feed-ingestion/rss-atom-ingestion/index.ts index 5c7ca28..9dd3f2e 100644 --- a/lib/data-feed-ingestion/rss-atom-ingestion/index.ts +++ b/lib/data-feed-ingestion/rss-atom-ingestion/index.ts @@ -3,46 +3,46 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Duration, RemovalPolicy } from 'aws-cdk-lib' -import { type Table } from 'aws-cdk-lib/aws-dynamodb' -import { Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3' -import { Construct } from 'constructs' -import { IngestionStepFunction } from './ingestion-step-function' -import { DataFeedPollStepFunction } from './data-feed-poll-step-function' -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' +import { Duration, RemovalPolicy } from 'aws-cdk-lib'; +import { type Table } from 'aws-cdk-lib/aws-dynamodb'; import { ApplicationLogLevel, Architecture, LambdaInsightsVersion, LoggingFormat, Runtime, - Tracing -} from 'aws-cdk-lib/aws-lambda' -import { NagSuppressions } from 'cdk-nag' + Tracing, +} from 'aws-cdk-lib/aws-lambda'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { DataFeedPollStepFunction } from './data-feed-poll-step-function'; +import { IngestionStepFunction } from './ingestion-step-function'; interface RssAtomFeedProps { - dataFeedTable: Table - dataFeedTableTypeIndex: string - dataFeedTableLSI: string - loggingBucket: Bucket + dataFeedTable: Table; + dataFeedTableTypeIndex: string; + dataFeedTableLSI: string; + loggingBucket: Bucket; } export class RssAtomFeedConstruct extends Construct { - public readonly ingestionStepFunction: IngestionStepFunction - public readonly dataFeedPollStepFunction: DataFeedPollStepFunction - public readonly feedSubscriberFunction: NodejsFunction - public readonly rssAtomDataBucket: Bucket + public readonly ingestionStepFunction: IngestionStepFunction; + public readonly dataFeedPollStepFunction: DataFeedPollStepFunction; + public readonly feedSubscriberFunction: NodejsFunction; + public readonly rssAtomDataBucket: Bucket; constructor (scope: Construct, id: string, props: RssAtomFeedProps) { - super(scope, id) - const { dataFeedTable, dataFeedTableTypeIndex, loggingBucket } = props + super(scope, id); + const { dataFeedTable, dataFeedTableTypeIndex, loggingBucket } = props; const rssAtomDataBucket = new Bucket(this, 'RssAtomDataBucket', { removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, serverAccessLogsBucket: loggingBucket, serverAccessLogsPrefix: 'rss-atom-feed-data-bucket-access-logs/', enforceSSL: true, - encryption: BucketEncryption.S3_MANAGED - }) + encryption: BucketEncryption.S3_MANAGED, + }); const ingestionStepFunction = new IngestionStepFunction( this, @@ -51,9 +51,9 @@ export class RssAtomFeedConstruct extends Construct { description: 'Step Function Responsible for ingesting data from RSS/ATOM feeds, generating summarizations and storing the information.', dataFeedTable, - rssAtomDataBucket - } - ) + rssAtomDataBucket, + }, + ); const dataFeedPollStepFunction = new DataFeedPollStepFunction( this, @@ -63,17 +63,14 @@ export class RssAtomFeedConstruct extends Construct { 'Step Function Responsible for getting enabled data feeds and starting ingestion for each one.', dataFeedIngestionStateMachine: ingestionStepFunction.stateMachine, dataFeedTable, - dataFeedTableTypeIndex - } - ) + dataFeedTableTypeIndex, + }, + ); const feedSubscriberFunction = new NodejsFunction(this, 'feed-subscriber', { description: 'Function responsible for subscribing to a specified RSS/ATOM feed', handler: 'handler', - entry: new URL( - import.meta.url.replace(/(.*)(\..+)/, '$1.' + 'feed-subscriber' + '$2') - ).pathname, architecture: Architecture.ARM_64, runtime: Runtime.NODEJS_20_X, tracing: Tracing.ACTIVE, @@ -84,23 +81,23 @@ export class RssAtomFeedConstruct extends Construct { POWERTOOLS_LOG_LEVEL: 'DEBUG', DATA_FEED_TABLE: dataFeedTable.tableName, INGESTION_STEP_FUNCTION: - ingestionStepFunction.stateMachine.stateMachineArn + ingestionStepFunction.stateMachine.stateMachineArn, }, - timeout: Duration.minutes(1) - }) + timeout: Duration.minutes(1), + }); - dataFeedTable.grantWriteData(feedSubscriberFunction) + dataFeedTable.grantWriteData(feedSubscriberFunction); ingestionStepFunction.stateMachine.grantStartExecution( - dataFeedPollStepFunction.stateMachine - ) + dataFeedPollStepFunction.stateMachine, + ); ingestionStepFunction.stateMachine.grantStartExecution( - feedSubscriberFunction - ) + feedSubscriberFunction, + ); - this.rssAtomDataBucket = rssAtomDataBucket - this.dataFeedPollStepFunction = dataFeedPollStepFunction - this.feedSubscriberFunction = feedSubscriberFunction - this.ingestionStepFunction = ingestionStepFunction + this.rssAtomDataBucket = rssAtomDataBucket; + this.dataFeedPollStepFunction = dataFeedPollStepFunction; + this.feedSubscriberFunction = feedSubscriberFunction; + this.ingestionStepFunction = ingestionStepFunction; /** * CDK NAG Suppressions @@ -110,10 +107,10 @@ export class RssAtomFeedConstruct extends Construct { [ { id: 'AwsSolutions-IAM5', - reason: 'Allowing CloudWatch & XRay' - } + reason: 'Allowing CloudWatch & XRay', + }, ], - true - ) + true, + ); } } diff --git a/lib/data-feed-ingestion/rss-atom-ingestion/ingestion-step-function.article-ingestor.ts b/lib/data-feed-ingestion/rss-atom-ingestion/ingestion-step-function.article-ingestor.ts index 27070ee..8f7ec77 100644 --- a/lib/data-feed-ingestion/rss-atom-ingestion/ingestion-step-function.article-ingestor.ts +++ b/lib/data-feed-ingestion/rss-atom-ingestion/ingestion-step-function.article-ingestor.ts @@ -3,86 +3,86 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ -import { Tracer } from '@aws-lambda-powertools/tracer' -import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware' -import { Logger } from '@aws-lambda-powertools/logger' -import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware' -import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics' -import { S3Client } from '@aws-sdk/client-s3' - -import { - DynamoDBClient, - PutItemCommand, - type PutItemCommandInput -} from '@aws-sdk/client-dynamodb' -import { Upload } from '@aws-sdk/lib-storage' -import { marshall } from '@aws-sdk/util-dynamodb' -import axios from 'axios' -import * as cheerio from 'cheerio' -import { v4 as uuidv4 } from 'uuid' -import middy from '@middy/core' -import { ArticleSummaryBuilder } from '../../shared/prompts/article-summary-prompt' -import { type MultiSizeFormattedResponse } from '../../shared/prompts/prompt-processing' +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; import { BedrockRuntimeClient, InvokeModelCommand, - InvokeModelCommandInput -} from '@aws-sdk/client-bedrock-runtime' + InvokeModelCommandInput, +} from '@aws-sdk/client-bedrock-runtime'; +import { + DynamoDBClient, + PutItemCommand, + type PutItemCommandInput, +} from '@aws-sdk/client-dynamodb'; +import { S3Client } from '@aws-sdk/client-s3'; -const SERVICE_NAME = 'article-ingestor' +import { Upload } from '@aws-sdk/lib-storage'; +import { marshall } from '@aws-sdk/util-dynamodb'; +import middy from '@middy/core'; +import axios from 'axios'; +import * as cheerio from 'cheerio'; +import { v4 as uuidv4 } from 'uuid'; +import { ArticleSummaryBuilder } from '../../shared/prompts/article-summary-prompt'; +import { type MultiSizeFormattedResponse } from '../../shared/prompts/prompt-processing'; -const tracer = new Tracer({ serviceName: SERVICE_NAME }) -const logger = new Logger({ serviceName: SERVICE_NAME }) -const metrics = new Metrics({ serviceName: SERVICE_NAME }) +const SERVICE_NAME = 'article-ingestor'; -const s3Client = tracer.captureAWSv3Client(new S3Client()) -const dynamodbClient = tracer.captureAWSv3Client(new DynamoDBClient()) +const tracer = new Tracer({ serviceName: SERVICE_NAME }); +const logger = new Logger({ serviceName: SERVICE_NAME }); +const metrics = new Metrics({ serviceName: SERVICE_NAME }); + +const s3Client = tracer.captureAWSv3Client(new S3Client()); +const dynamodbClient = tracer.captureAWSv3Client(new DynamoDBClient()); const bedrockRuntimeClient = tracer.captureAWSv3Client( - new BedrockRuntimeClient() -) + new BedrockRuntimeClient(), +); -const BEDROCK_MODEL_ID = 'anthropic.claude-3-sonnet-20240229-v1:0' -const NEWS_DATA_INGEST_BUCKET = process.env.NEWS_DATA_INGEST_BUCKET -const DATA_FEED_TABLE = process.env.DATA_FEED_TABLE +const BEDROCK_MODEL_ID = 'anthropic.claude-3-sonnet-20240229-v1:0'; +const NEWS_DATA_INGEST_BUCKET = process.env.NEWS_DATA_INGEST_BUCKET; +const DATA_FEED_TABLE = process.env.DATA_FEED_TABLE; interface ArticleIngestorInput { - summarizationPrompt?: string + summarizationPrompt?: string; input: { - url: string + url: string; dataFeed: { - id: string - } - id?: string - title: string + id: string; + }; + id?: string; + title: string; account: { - id: string - __typename: 'Account' - } - } + id: string; + __typename: 'Account'; + }; + }; } const lambdaHander = async (event: ArticleIngestorInput): Promise => { - const { dataFeed, account, id: inputArticleId, url, title } = event.input - const summarizationPrompt = event.summarizationPrompt - const subsegment = tracer.getSegment()?.addNewSubsegment('### ingestArticle') + const { dataFeed, account, id: inputArticleId, url, title } = event.input; + const summarizationPrompt = event.summarizationPrompt; + const subsegment = tracer.getSegment()?.addNewSubsegment('### ingestArticle'); if (subsegment !== undefined) { - tracer.setSegment(subsegment) + tracer.setSegment(subsegment); } try { if (url === undefined) { - throw new Error('No url to crawl') + throw new Error('No url to crawl'); } - const articleId = inputArticleId?.trim() ?? uuidv4() - const $ = await getSiteContent(url) - let articleText: string = '' + const articleId = inputArticleId?.trim() ?? uuidv4(); + const $ = await getSiteContent(url); + let articleText: string = ''; if ($('article').length > 0) { - articleText = $('article').text() + articleText = $('article').text(); } else { - articleText = $('body').text() + articleText = $('body').text(); } if (articleText !== undefined) { try { - await storeSiteContent(articleText, dataFeed.id, articleId) + await storeSiteContent(articleText, dataFeed.id, articleId); } catch (error) { logger.error('Failed to store site contents to s3 ', { error, @@ -90,52 +90,52 @@ const lambdaHander = async (event: ArticleIngestorInput): Promise => { dataFeed, articleId, title, - articleText - }) + articleText, + }); } - let response + let response; try { response = await generateArticleSummarization( articleText, - summarizationPrompt - ) + summarizationPrompt, + ); } catch (error) { logger.error('Failed to generate article summary for ' + url, { - error - }) - tracer.addErrorAsMetadata(error as Error) - logger.debug('Attempting to find a fallback URL') - const redirectFallback = checkForRedirectFallback($) + error, + }); + tracer.addErrorAsMetadata(error as Error); + logger.debug('Attempting to find a fallback URL'); + const redirectFallback = checkForRedirectFallback($); if (redirectFallback !== null) { - const $$ = await getSiteContent(url) + const $$ = await getSiteContent(url); if ($$('article').length > 0) { - articleText = $$('article').text() + articleText = $$('article').text(); } else { - articleText = $$('body').text() + articleText = $$('body').text(); } if (articleText !== undefined && articleText.length > 255) { try { - await storeSiteContent(articleText, dataFeed.id, articleId) - } catch (error) { + await storeSiteContent(articleText, dataFeed.id, articleId); + } catch (subError) { logger.error('Failed to store site contents to s3 ', { - error, + subError, url, dataFeed, articleId, title, - articleText - }) + articleText, + }); } try { response = await generateArticleSummarization( articleText, - summarizationPrompt - ) - } catch (error) { + summarizationPrompt, + ); + } catch (subError) { logger.error('Failed to generate article summary for ' + url, { - error - }) - tracer.addErrorAsMetadata(error as Error) + subError, + }); + tracer.addErrorAsMetadata(subError as Error); } } } @@ -148,86 +148,86 @@ const lambdaHander = async (event: ArticleIngestorInput): Promise => { account.id, url, title, - summarizationPrompt - ) + summarizationPrompt, + ); } } } else { - logger.error('Failed to generate article summary for ' + url) - tracer.putAnnotation('summaryGenerated', false) - metrics.addMetric('EmptyArticleFound', MetricUnits.Count, 1) + logger.error('Failed to generate article summary for ' + url); + tracer.putAnnotation('summaryGenerated', false); + metrics.addMetric('EmptyArticleFound', MetricUnit.Count, 1); } } catch (error) { - logger.error('Error in website crawler', { error }) - tracer.addErrorAsMetadata(error as Error) + logger.error('Error in website crawler', { error }); + tracer.addErrorAsMetadata(error as Error); } -} +}; const getSiteContent = async (url: string): Promise => { - logger.debug(`getSiteContent Called; url = ${url}`) - tracer.putMetadata('url', url) - let $: cheerio.Root + logger.debug(`getSiteContent Called; url = ${url}`); + tracer.putMetadata('url', url); + let $: cheerio.Root; try { - logger.debug('URL of Provided Site = ' + url) - const response = await axios.get(url) - tracer.putAnnotation('url', 'Successfully Crawled') - const text = response.data as string - $ = cheerio.load(text) + logger.debug('URL of Provided Site = ' + url); + const response = await axios.get(url); + tracer.putAnnotation('url', 'Successfully Crawled'); + const text = response.data as string; + $ = cheerio.load(text); // Cutting out elements that aren't needed - $('footer').remove() - $('header').remove() - $('script').remove() - $('style').remove() - $('nav').remove() + $('footer').remove(); + $('header').remove(); + $('script').remove(); + $('style').remove(); + $('nav').remove(); } catch (error) { - logger.error(`Failed to crawl; url = ${url}`) - logger.error(JSON.stringify(error)) - tracer.addErrorAsMetadata(error as Error) - throw error + logger.error(`Failed to crawl; url = ${url}`); + logger.error(JSON.stringify(error)); + tracer.addErrorAsMetadata(error as Error); + throw error; } - return $ -} + return $; +}; const storeSiteContent = async ( text: string, dataFeedId: string, - articleId: string + articleId: string, ): Promise => { - metrics.addMetric('TextsStored', MetricUnits.Count, 1) + metrics.addMetric('TextsStored', MetricUnit.Count, 1); - const body = Buffer.from(text) + const body = Buffer.from(text); const parallelUpload = new Upload({ client: s3Client, params: { Bucket: NEWS_DATA_INGEST_BUCKET, Key: `${dataFeedId}/${articleId}`, - Body: body - } - }) - logger.debug('Starting upload') + Body: body, + }, + }); + logger.debug('Starting upload'); try { - await parallelUpload.done() - tracer.putAnnotation('uploadComplete', true) + await parallelUpload.done(); + tracer.putAnnotation('uploadComplete', true); tracer.putMetadata('S3SiteContents', { bucket: NEWS_DATA_INGEST_BUCKET, - key: `${dataFeedId}/${articleId}` - }) + key: `${dataFeedId}/${articleId}`, + }); } catch (error) { - tracer.addErrorAsMetadata(error as Error) - tracer.putAnnotation('uploadComplete', false) + tracer.addErrorAsMetadata(error as Error); + tracer.putAnnotation('uploadComplete', false); } -} +}; const generateArticleSummarization = async ( articleBody: string, - summarizationPrompt?: string + summarizationPrompt?: string, ): Promise => { const summaryBuilder = new ArticleSummaryBuilder( articleBody, - summarizationPrompt ?? null - ) - const prompt = summaryBuilder.getCompiledPrompt() - console.debug(prompt) + summarizationPrompt ?? null, + ); + const prompt = summaryBuilder.getCompiledPrompt(); + console.debug(prompt); const input: InvokeModelCommandInput = { modelId: BEDROCK_MODEL_ID, contentType: 'application/json', @@ -236,31 +236,31 @@ const generateArticleSummarization = async ( JSON.stringify({ anthropic_version: 'bedrock-2023-05-31', max_tokens: 1000, - messages: [{ role: 'user', content: prompt }] - }) - ) - } - const command = new InvokeModelCommand(input) - const response = await bedrockRuntimeClient.send(command) - logger.debug('GenAI Output', { response }) - const responseText = new TextDecoder().decode(response.body) - const responseObject = JSON.parse(responseText) + messages: [{ role: 'user', content: prompt }], + }), + ), + }; + const command = new InvokeModelCommand(input); + const response = await bedrockRuntimeClient.send(command); + logger.debug('GenAI Output', { response }); + const responseText = new TextDecoder().decode(response.body); + const responseObject = JSON.parse(responseText); const processedResponse = summaryBuilder.getProcessedResponse( responseObject.content .map((item: { type: string; text: any }) => { - return item.type === 'text' ? item.text : '' + return item.type === 'text' ? item.text : ''; }) - .join('\n') - ) - logger.debug('Formatted response from Model:', { processedResponse }) + .join('\n'), + ); + logger.debug('Formatted response from Model:', { processedResponse }); if (processedResponse.error.response !== null) { logger.error('Error in processed response from LLM', { - processedResponse - }) - throw new Error('Error in processed response from LLM') + processedResponse, + }); + throw new Error('Error in processed response from LLM'); } - return processedResponse -} + return processedResponse; +}; const saveArticleData = async ( processedResponse: MultiSizeFormattedResponse, @@ -269,14 +269,14 @@ const saveArticleData = async ( accountId: string, url: string, title: string, - summarizationPrompt?: string + summarizationPrompt?: string, ): Promise => { - tracer.putMetadata('dataFeedId', dataFeedId, 'articleInfo') - tracer.putMetadata('articleId', articleId, 'articleInfo') - tracer.putMetadata('url', url, 'articleInfo') - tracer.putMetadata('title', title, 'articleInfo') - tracer.putMetadata('summarizationPrompt', summarizationPrompt, 'articleInfo') - const { keywords, shortSummary, longSummary } = processedResponse + tracer.putMetadata('dataFeedId', dataFeedId, 'articleInfo'); + tracer.putMetadata('articleId', articleId, 'articleInfo'); + tracer.putMetadata('url', url, 'articleInfo'); + tracer.putMetadata('title', title, 'articleInfo'); + tracer.putMetadata('summarizationPrompt', summarizationPrompt, 'articleInfo'); + const { keywords, shortSummary, longSummary } = processedResponse; const input: PutItemCommandInput = { TableName: DATA_FEED_TABLE, Item: marshall( @@ -291,39 +291,38 @@ const saveArticleData = async ( summarizationPrompt, keywords: keywords.response, shortSummary: shortSummary.response, - longSummary: longSummary.response + longSummary: longSummary.response, }, { - removeUndefinedValues: true - } - ) - } - const command = new PutItemCommand(input) - const response = await dynamodbClient.send(command) - logger.debug(JSON.stringify(response)) - metrics.addMetric('ArticlesSavedToDDB', MetricUnits.Count, 1) -} + removeUndefinedValues: true, + }, + ), + }; + const command = new PutItemCommand(input); + const response = await dynamodbClient.send(command); + logger.debug(JSON.stringify(response)); + metrics.addMetric('ArticlesSavedToDDB', MetricUnit.Count, 1); +}; const checkForRedirectFallback = ($: cheerio.Root): string | null => { - logger.debug('Checking for redirect fallback in