diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..5894379 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# See: https://help.github.com/articles/about-codeowners/ + +# These owners will be the default owners for everything in the repo. +* @bhashinee diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..2c4f082 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## Purpose + +Fixes: + +## Examples + +## Checklist +- [ ] Linked to an issue +- [ ] Updated the specification +- [ ] Updated the changelog +- [ ] Added tests +- [ ] Checked native-image compatibility \ No newline at end of file diff --git a/.github/workflows/build-with-bal-test-graalvm.yml b/.github/workflows/build-with-bal-test-graalvm.yml new file mode 100644 index 0000000..bf62a1e --- /dev/null +++ b/.github/workflows/build-with-bal-test-graalvm.yml @@ -0,0 +1,19 @@ +name: GraalVM Check + +on: + schedule: + - cron: "30 18 * * *" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + call_stdlib_workflow: + name: Run StdLib Workflow + if: ${{ github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository_owner == 'ballerina-platform') }} + uses: ballerina-platform/ballerina-standard-library/.github/workflows/build-with-bal-test-graalvm-connector-template.yml@main + secrets: inherit + with: + additional-build-flags: "-x :aws.dynamodbstreams-examples:build" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8d2b374 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + branches: + - main + - 2201.[0-9]+.x + repository_dispatch: + types: check_connector_for_breaking_changes + +jobs: + call_workflow: + name: Run Connector Build Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-standard-library/.github/workflows/build-connector-template.yml@main + secrets: inherit + with: + repo-name: module-ballerinax-aws.dynamodbstreams diff --git a/.github/workflows/daily-build.yml b/.github/workflows/daily-build.yml new file mode 100644 index 0000000..28c44c4 --- /dev/null +++ b/.github/workflows/daily-build.yml @@ -0,0 +1,14 @@ +name: Daily build + +on: + schedule: + - cron: "30 2 * * *" + +jobs: + call_workflow: + name: Run Daily Build Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-standard-library/.github/workflows/daily-build-connector-template.yml@main + secrets: inherit + with: + repo-name: module-ballerinax-aws.dynamodbstreams diff --git a/.github/workflows/dev-stg-release.yml b/.github/workflows/dev-stg-release.yml new file mode 100644 index 0000000..f43685c --- /dev/null +++ b/.github/workflows/dev-stg-release.yml @@ -0,0 +1,22 @@ +name: Publish to the Ballerina Dev\Stage Central + +on: + workflow_dispatch: + inputs: + environment: + type: choice + description: Select Environment + required: true + options: + - DEV CENTRAL + - STAGE CENTRAL + +jobs: + call_workflow: + name: Run Dev\Stage Central Publish Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-standard-library/.github/workflows/dev-stage-central-publish-connector-template.yml@main + secrets: inherit + with: + environment: ${{ github.event.inputs.environment }} + additional-publish-flags: "-x :aws.dynamodbstreams-examples:build" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..5dda019 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,14 @@ +name: PR Build + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +on: pull_request + +jobs: + call_workflow: + name: Run PR Build Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-standard-library/.github/workflows/pr-build-connector-template.yml@main + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2b8744d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,19 @@ +name: Publish Release + +on: + workflow_dispatch: + repository_dispatch: + types: [ stdlib-release-pipeline ] + +jobs: + call_workflow: + name: Run Release Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-standard-library/.github/workflows/release-package-connector-template.yml@main + secrets: inherit + with: + package-name: aws.dynamodbstreams + package-org: ballerinax + additional-build-flags: "-x :aws.dynamodbstreams-examples:build" + additional-release-flags: "-x :aws.dynamodbstreams-examples:build" + additional-publish-flags: "-x :aws.dynamodbstreams-examples:build" diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 0000000..a1d297c --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,15 @@ +name: Trivy + +on: + workflow_dispatch: + schedule: + - cron: "30 20 * * *" + +jobs: + call_workflow: + name: Run Trivy Scan Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-standard-library/.github/workflows/trivy-scan-template.yml@main + secrets: inherit + with: + additional-build-flags: "-x :aws.dynamodbstreams-examples:build" diff --git a/README.md b/README.md index 7338838..d604c35 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,113 @@ -# module-ballerinax-aws.dynamodbstreams -This Ballerina module facilitates to interact with the AWS DynamoDB Streams. +# Ballerina Amazon DynamoDB Streams Connector +[![Build Status](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams/workflows/CI/badge.svg)](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/ballerina-platform/module-ballerinax-aws.dynamodbstreams/branch/main/graph/badge.svg)](https://codecov.io/gh/ballerina-platform/module-ballerinax-aws.dynamodbstreams) +[![GitHub Last Commit](https://img.shields.io/github/last-commit/ballerina-platform/module-ballerinax-aws.dynamodbstreams.svg)](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams./commits/master) +[![GraalVM Check](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams/actions/workflows/build-with-bal-test-native.yml/badge.svg)](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams/actions/workflows/build-with-bal-test-native.yml) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +[Amazon DynamoDB](https://aws.amazon.com/dynamodb/) is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale. DynamoDB offers built-in security, continuous backups, automated multi-region replication, in-memory caching, and data export tools. + +The connector provides the capability to programmatically handle AWS DynamoDB Streams related operations. + +For more information, go to the module(s). +- [aws.dynamodbstreams](./Module.md) + +## Set up DynamoDB credentials + +To invoke the DynamoDB REST API, you need AWS credentials. Below is a step-by-step guide on how to obtain these credentials: + +1. Create an AWS Account: +* If you don't already have an AWS account, you need to create one. Go to the AWS Management Console, click on "Create an AWS Account," and follow the instructions. + +2. Access the AWS Identity and Access Management (IAM) Console: + +* Once logged into the [AWS Management Console](https://aws.amazon.com/), go to the IAM console by selecting "Services" and then choosing "IAM" under the "Security, Identity, & Compliance" section. + +3. Create an IAM User: + +* In the IAM console, navigate to "Users" and click on "Add user." +* Enter a username, and under "Select AWS access type," choose "Programmatic access." +* Click through the permissions setup, attaching policies that grant access to DynamoDB if you have specific requirements. +* Review the details and click "Create user." + +4. Access Key ID and Secret Access Key: + +* Once the user is created, you will see a success message. Take note of the "Access key ID" and "Secret access key" displayed on the confirmation screen. These credentials are needed to authenticate your requests. + +5. Securely Store Credentials: + +* Download the CSV file containing the credentials, or copy the "Access key ID" and "Secret access key" to a secure location. This information is sensitive and should be handled with care. + +6. Use the Credentials in Your Application: + +* In your application, use the obtained "Access key ID" and "Secret access key" to authenticate requests to the DynamoDB REST API. + +## Quickstart + +**Note**: Ensure you follow the [prerequisites](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams#set-up-dynamodb-credentials) to get the credentials to be used. + +To use the `dynamodbstreams` connector in your Ballerina application, modify the `.bal` file as follows: + +### Step 1: Import the connector +Import the `ballerinax/aws.dynamodbstreams` package into your Ballerina project. +```ballerina +import ballerinax/aws.dynamodbstreams; +``` + +### Step 2: Instantiate a new connector +Create a `dynamodbstreams:ConnectionConfig` with the obtained access key id and secret access key to initialize the connector with it. +```ballerina +dynamodbstreams:ConnectionConfig amazonDynamodbConfig = { + awsCredentials: { + accessKeyId: "ACCESS_KEY_ID", + secretAccessKey: "SECRET_ACCESS_KEY" + }, + region: "REGION" +}; + +dynamodbstreams:Client amazonDynamodbClient = check new(amazonDynamodbConfig); +``` + +### Step 3: Invoke connector operation +1. Now you can use the operations available within the connector. Note that they are in the form of remote operations. +Following is an example of how to describe a stream in DynamoDB streams using the connector. + +```ballerina +public function main() returns error? { + dynamodbstreams:DescribeStreamInput describeStreamInput = { + streamArn: "arn:aws:dynamodb:us-east-1:134633749276:table/TestStreamTable/stream/2024-01-04T04:43:13.919" + }; + dynamodbstreams:StreamDescription response = check dynamoDBStreamClient->describeStream(describeStreamInput); +} +``` +2. Use `bal run` command to compile and run the Ballerina program. + + +## Examples + +The `dynamodbstreams` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams/tree/master/examples). + +1. [Real-time order processing](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams/tree/master/examples/order-management/client.bal) + A real-time order processing system. + +For comprehensive information about the connector's functionality, configuration, and usage in Ballerina programs, refer to the `dynamodbstreams` connector's reference guide in [Ballerina Central](https://central.ballerina.io/ballerinax/aws.dynamodbstreams/latest). + +## Building from the source + +### Setting up the prerequisites + +1. Download and install Java SE Development Kit (JDK) version 17. You can install either [OpenJDK](https://adoptopenjdk.net/) or [Oracle](https://www.oracle.com/java/technologies/downloads/). + > **Note:** Set the JAVA_HOME environment variable to the path name of the directory into which you installed JDK. +2. Download and install [Ballerina Swan Lake](https://ballerina.io/). + +### Building the source +Execute the commands below to build from the source. +- To build the library: + ```shell + ./gradlew clean build + ``` +- To run the integration tests: + ```shell + ./gradlew clean test + ``` + \ No newline at end of file diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml new file mode 100644 index 0000000..4917fb6 --- /dev/null +++ b/ballerina/Ballerina.toml @@ -0,0 +1,12 @@ +[package] +distribution = "2201.8.0" +org = "ballerinax" +name = "aws.dynamodbstreams" +version = "1.0.0" +authors = ["Ballerina"] +repository = "https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams" +icon = "icon.png" +license = ["Apache-2.0"] + +[build-options] +observabilityIncluded = true diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml new file mode 100644 index 0000000..b387126 --- /dev/null +++ b/ballerina/Dependencies.toml @@ -0,0 +1,378 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.8.3" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.7.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "crypto", moduleName = "crypto"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.10.5" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" +modules = [ + {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] +modules = [ + {org = "ballerina", packageName = "lang.array", moduleName = "lang.array"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.error" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.2.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "os", moduleName = "os"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "test" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.error"} +] +modules = [ + {org = "ballerina", packageName = "test", moduleName = "test"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "time", moduleName = "time"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "url", moduleName = "url"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "aws.dynamodb" +version = "2.2.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "client.config"} +] +modules = [ + {org = "ballerinax", packageName = "aws.dynamodb", moduleName = "aws.dynamodb"} +] + +[[package]] +org = "ballerinax" +name = "aws.dynamodbstreams" +version = "1.0.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "test"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "aws.dynamodb"}, + {org = "ballerinax", name = "client.config"} +] +modules = [ + {org = "ballerinax", packageName = "aws.dynamodbstreams", moduleName = "aws.dynamodbstreams"} +] + +[[package]] +org = "ballerinax" +name = "client.config" +version = "1.0.1" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "oauth2"} +] +modules = [ + {org = "ballerinax", packageName = "client.config", moduleName = "client.config"} +] + diff --git a/ballerina/Module.md b/ballerina/Module.md new file mode 100644 index 0000000..e9729c0 --- /dev/null +++ b/ballerina/Module.md @@ -0,0 +1,47 @@ +## Overview +The Ballerina AWS DynamoDB streams connector provides the capability to programatically handle [AWS DynamoDB streams](https://aws.amazon.com/dynamodb/) related operations. + +This module supports [Amazon DynamoDB REST API 20120810](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/Welcome.html). + +## Prerequisites +Before using this connector in your Ballerina application, complete the following: +1. Create an [AWS account](https://portal.aws.amazon.com/billing/signup?nc2=h_ct&src=default&redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start) +2. [Obtain tokens](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) + +## Quickstart +To use the AWS DynamoDB streams connector in your Ballerina application, update the .bal file as follows: + +### Step 1: Import connector +Import the `ballerinax/aws.dynamodbstreams` module into the Ballerina project. +```ballerina +import ballerinax/aws.dynamodbstreams; +``` + +### Step 2: Create a new connector instance +Create an `dynamodbstreams:ConnectionConfig` with the tokens obtained, and initialize the connector with it. +```ballerina +dynamodbstreams:ConnectionConfig amazonDynamodbConfig = { + awsCredentials: { + accessKeyId: "", + secretAccessKey: "" + + }, + region: "" +}; + +dynamodbstreams:Client amazonDynamoDBClient = check new(amazonDynamodbConfig); +``` + +### Step 3: Invoke connector operation +1. Now you can use the operations available within the connector. Note that they are in the form of remote operations. +Following is an example on how to describe a stream in DynamoDB streams using the connector. + +```ballerina +public function main() returns error? { + dynamodbstreams:DescribeStreamInput describeStreamInput = { + streamArn: "arn:aws:dynamodb:us-east-1:134633749276:table/TestStreamTable/stream/2024-01-04T04:43:13.919" + }; + dynamodbstreams:StreamDescription response = check dynamoDBStreamClient->describeStream(describeStreamInput); +} +``` +2. Use `bal run` command to compile and run the Ballerina program. diff --git a/ballerina/Package.md b/ballerina/Package.md new file mode 100644 index 0000000..2aec907 --- /dev/null +++ b/ballerina/Package.md @@ -0,0 +1,83 @@ +## Package overview + +The `ballerinax/aws.dynamodbstreams` is a [Ballerina](https://ballerina.io/) connector for AWS DynamoDB. It is comprised of the following capabilities. +* Perform AWS DynamoDB Streams related operations programmatically. The `ballerinax/aws.dynamodbstreams` module provides this capability. + +## Set up DynamoDB credentials + +To invoke the DynamoDB REST API, you need AWS credentials. Below is a step-by-step guide on how to obtain these credentials: + +1. Create an AWS Account: +* If you don't already have an AWS account, you need to create one. Go to the AWS Management Console, click on "Create an AWS Account," and follow the instructions. + +2. Access the AWS Identity and Access Management (IAM) Console: + +* Once logged into the [AWS Management Console](https://aws.amazon.com/), go to the IAM console by selecting "Services" and then choosing "IAM" under the "Security, Identity, & Compliance" section. + +3. Create an IAM User: + +* In the IAM console, navigate to "Users" and click on "Add user." +* Enter a username, and under "Select AWS access type," choose "Programmatic access." +* Click through the permissions setup, attaching policies that grant access to DynamoDB if you have specific requirements. +* Review the details and click "Create user." + +4. Access Key ID and Secret Access Key: + +* Once the user is created, you will see a success message. Take note of the "Access key ID" and "Secret access key" displayed on the confirmation screen. These credentials are needed to authenticate your requests. + +5. Securely Store Credentials: + +* Download the CSV file containing the credentials, or copy the "Access key ID" and "Secret access key" to a secure location. This information is sensitive and should be handled with care. + +6. Use the Credentials in Your Application: + +* In your application, use the obtained "Access key ID" and "Secret access key" to authenticate requests to the DynamoDB REST API. + +## Quickstart + +**Note**: Ensure you follow the [prerequisites](https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams#set-up-dynamodb-credentials) to get the credentials to be used. + +To use the `dynamodbstreams` connector in your Ballerina application, modify the `.bal` file as follows: + +### Step 1: Import the connector +Import the `ballerinax/aws.dynamodbstreams` package into your Ballerina project. +```ballerina +import ballerinax/aws.dynamodbstreams; +``` + +### Step 2: Instantiate a new connector +Create a `dynamodbstreams:ConnectionConfig` with the obtained access key id and secret access key to initialize the connector with it. +```ballerina +dynamodbstreams:ConnectionConfig amazonDynamodbConfig = { + awsCredentials: { + accessKeyId: "ACCESS_KEY_ID", + secretAccessKey: "SECRET_ACCESS_KEY" + }, + region: "REGION" +}; + +dynamodbstreams:Client amazonDynamodbClient = check new(amazonDynamodbConfig); +``` + +### Step 3: Invoke connector operation +1. Now you can use the operations available within the connector. Note that they are in the form of remote operations. +Following is an example of how to describe a stream in DynamoDB streams using the connector. + +```ballerina +public function main() returns error? { + dynamodbstreams:DescribeStreamInput describeStreamInput = { + streamArn: "arn:aws:dynamodb:us-east-1:134633749276:table/TestStreamTable/stream/2024-01-04T04:43:13.919" + }; + dynamodbstreams:StreamDescription response = check dynamoDBStreamClient->describeStream(describeStreamInput); +} +``` +2. Use `bal run` command to compile and run the Ballerina program. + +## Report issues + +To report bugs, request new features, start new discussions, view project boards, etc., go to the [Ballerina standard library parent repository](https://github.com/ballerina-platform/ballerina-standard-library). + +## Useful links + +- Chat live with us via our [Discord server](https://discord.gg/ballerinalang). +- Post all technical questions on Stack Overflow with the [#ballerina](https://stackoverflow.com/questions/tagged/ballerina) tag. diff --git a/ballerina/build.gradle b/ballerina/build.gradle new file mode 100644 index 0000000..a141858 --- /dev/null +++ b/ballerina/build.gradle @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.tools.ant.taskdefs.condition.Os + +buildscript { + repositories { + maven { + url = 'https://maven.pkg.github.com/ballerina-platform/plugin-gradle' + credentials { + username System.getenv("packageUser") + password System.getenv("packagePAT") + } + } + } + dependencies { + classpath "io.ballerina:plugin-gradle:${project.ballerinaGradlePluginVersion}" + } +} + +plugins { + id 'io.ballerina.plugin' +} + +description = 'Dynamodb Streams - Ballerina' + +def packageName = "aws.dynamodbstreams" +def packageOrg = "ballerinax" +def tomlVersion = stripBallerinaExtensionVersion("${project.version}") +def ballerinaTomlFilePlaceHolder = new File("${project.rootDir}/build-config/resources/Ballerina.toml") +def ballerinaTomlFile = new File("$project.projectDir/Ballerina.toml") + +def stripBallerinaExtensionVersion(String extVersion) { + if (extVersion.matches(project.ext.timestampedVersionRegex)) { + def splitVersion = extVersion.split('-') + if (splitVersion.length > 3) { + def strippedValues = splitVersion[0..-4] + return strippedValues.join('-') + } else { + return extVersion + } + } else { + return extVersion.replace("${project.ext.snapshotVersion}", "") + } +} + +ballerina { + packageOrganization = packageOrg + module = packageName + testCoverageParam = "--code-coverage --coverage-format=xml" + buildOnDockerImage = "nightly" +} + +task updateTomlFiles { + doLast { + def newBallerinaToml = ballerinaTomlFilePlaceHolder.text.replace("@project.version@", project.version) + newBallerinaToml = newBallerinaToml.replace("@toml.version@", tomlVersion) + ballerinaTomlFile.text = newBallerinaToml + } +} + +task commitTomlFiles { + doLast { + project.exec { + ignoreExitValue true + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'cmd', '/c', "git commit -m \"[Automated] Update the toml files\" Ballerina.toml Dependencies.toml" + } else { + commandLine 'sh', '-c', "git commit -m '[Automated] Update the toml files' Ballerina.toml Dependencies.toml" + } + } + } +} + +clean { + def folder = file('build') + if( folder.exists() ) { + delete 'build' + } +} + +publishToMavenLocal.dependsOn build +publish.dependsOn build diff --git a/ballerina/client.bal b/ballerina/client.bal new file mode 100644 index 0000000..69eaf81 --- /dev/null +++ b/ballerina/client.bal @@ -0,0 +1,101 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; +import ballerinax/'client.config; + +# The Ballerina AWS DynamoDB Streams connector provides the capability to access AWS DynamoDB Streams related operations. +@display {label: "Amazon DynamoDB Streams", iconPath: "icon.png"} +public isolated client class Client { + private final http:Client awsDynamoDb; + private final string accessKeyId; + private final string secretAccessKey; + private final string? securityToken; + private final string region; + private final string awsHost; + private final string uri = SLASH; + + # Initializes the connector. During initialization you have to pass access key id, secret access key, and region. + # Create an AWS account and obtain tokens following + # [this guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html). + # + # + config - Configuration required to initialize the client + # + return - An error on failure of initialization or else `()` + public isolated function init(ConnectionConfig config) returns error? { + self.accessKeyId = config.awsCredentials.accessKeyId; + self.secretAccessKey = config.awsCredentials.secretAccessKey; + self.securityToken = config.awsCredentials?.securityToken; + self.region = config.region; + self.awsHost = AWS_STREAMS_SERVICE + DOT + self.region + DOT + AWS_HOST; + string endpoint = HTTPS + self.awsHost; + + http:ClientConfiguration httpClientConfig = check config:constructHTTPClientConfig(config); + self.awsDynamoDb = check new (endpoint, httpClientConfig); + } + + # Returns an array of stream ARNs associated with the current account and endpoint. If the TableName parameter is present, then ListStreams will return only the streams ARNs for that table. + # + # + request - The required details to list the streams + # + return - If success, `stream` or else an error + remote isolated function listStreams(ListStreamsInput request) returns stream|error { + ListStream listStream = check new ListStream(self.awsDynamoDb, self.awsHost, self.accessKeyId, + self.secretAccessKey, self.region, request); + return new stream(listStream); + } + + # Returns information about a stream, including the current status of the stream, its Amazon Resource Name (ARN), the composition of its shards, and its corresponding DynamoDB table. + # + # + request - The required details to describe the stream + # + return - If success, dynamodb:StreamDescription record, else an error + remote isolated function describeStream(DescribeStreamInput request) returns StreamDescription|error { + string target = STREAMS_VERSION + DOT + "DescribeStream"; + json payload = check request.cloneWithType(json); + convertJsonKeysToUpperCase(payload); + map signedRequestHeaders = check getSignedRequestHeaders(self.awsHost, self.accessKeyId, + self.secretAccessKey, self.region, + POST, self.uri, target, payload); + json response = check self.awsDynamoDb->post(self.uri, payload, signedRequestHeaders); + convertJsonKeysToCamelCase(response); + json streamDescription = check response.streamDescription; + return check streamDescription.cloneWithType(StreamDescription); + } + + # Returns a shard iterator. A shard iterator provides information about how to retrieve the stream records from within a shard. Use the shard iterator in a subsequent GetRecords request to read the stream records from the shard. + # + # + request - The required details to get the shard iterator + # + return - If success, ShardIterator, else an error + remote isolated function getShardIterator(GetShardsIteratorInput request) returns GetShardsIteratorOutput|error { + string target = STREAMS_VERSION + DOT + "GetShardIterator"; + json payload = check request.cloneWithType(json); + convertJsonKeysToUpperCase(payload); + map signedRequestHeaders = check getSignedRequestHeaders(self.awsHost, self.accessKeyId, + self.secretAccessKey, self.region, + POST, self.uri, target, payload); + json response = check self.awsDynamoDb->post(self.uri, payload, signedRequestHeaders); + convertJsonKeysToCamelCase(response); + return check response.cloneWithType(GetShardsIteratorOutput); + } + + # Retrieves the stream records from a given shard. + # + # + request - The required details to get the records from a shard + # + return - If success, ShardIterator, else an error + remote isolated function getRecords(GetRecordsInput request) returns stream|error { + RecordsStream recordStream = check new RecordsStream(self.awsDynamoDb, self.awsHost, self.accessKeyId, + self.secretAccessKey, self.region, request); + return new stream(recordStream); + } +} diff --git a/ballerina/constants.bal b/ballerina/constants.bal new file mode 100644 index 0000000..772c1ec --- /dev/null +++ b/ballerina/constants.bal @@ -0,0 +1,114 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +const string AWS_HOST = "amazonaws.com"; +const string AWS_SERVICE = "dynamodb"; +const string AWS_STREAMS_SERVICE = "streams.dynamodb"; +const string STREAMS_VERSION = "DynamoDBStreams_20120810"; + +const string UTF_8 = "UTF-8"; +const string HOST = "host"; +const string CONTENT_TYPE = "content-type"; +const string APPLICATION_JSON = "application/json"; +const string X_AMZ_DATE = "x-amz-date"; +const string X_AMZ_TARGET = "x-amz-target"; +const string AWS4_REQUEST = "aws4_request"; +const string AWS4_HMAC_SHA256 = "AWS4-HMAC-SHA256"; +const string CREDENTIAL = "Credential"; +const string SIGNED_HEADER = "SignedHeaders"; +const string SIGNATURE = "Signature"; +const string AWS4 = "AWS4"; +const string ISO8601_BASIC_DATE_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; +const string SHORT_DATE_FORMAT = "yyyyMMdd"; +const string ENCODED_SLASH = "%2F"; +const string SLASH = "/"; +const string EMPTY_STRING = ""; +const string NEW_LINE = "\n"; +const string COLON = ":"; +const string SEMICOLON = ";"; +const string EQUAL = "="; +const string SPACE = " "; +const string COMMA = ","; +const string DOT = "."; +const string Z = "Z"; + +// HTTP. +const string POST = "POST"; +const string HTTPS = "https://"; + +// Constants to refer the headers. +const string HEADER_CONTENT_TYPE = "Content-Type"; +const string HEADER_X_AMZ_CONTENT_SHA256 = "X-Amz-Content-Sha256"; +const string HEADER_X_AMZ_DATE = "X-Amz-Date"; +const string HEADER_X_AMZ_TARGET = "X-Amz-Target"; +const string HEADER_HOST = "Host"; +const string HEADER_AUTHORIZATION = "Authorization"; + +const string GENERATE_SIGNED_REQUEST_HEADERS_FAILED_MSG = "Error occurred while generating signed request headers."; + +# The role that this key attribute will assume. +public enum KeyType { + # The partition key + HASH, + # The sort key + RANGE +} + +# The format of the records within this stream +public enum StreamViewType { + # The entire item, as it appeared after it was modified + NEW_IMAGE, + # The entire item, as it appeared before it was modified + OLD_IMAGE, + # Both the new and the old item images of the item + NEW_AND_OLD_IMAGES, + # Only the key attributes of the modified item + KEYS_ONLY +} + +# The current state of the stream +public enum StreamStatus { + # The stream is being created + ENABLING, + # The stream is enabled + ENABLED, + # The stream is being deleted + DISABLING, + # The stream is disabled + DISABLED +} + +# The type of action. +public enum eventName { + # A new item was added to the table + INSERT, + # An item was modified + MODIFY, + # An item was deleted from the table + REMOVE +} + +# The type of the shard. +public enum ShardIteratorType { + # Start reading at the last untrimmed record in the shard in the system, which is the oldest data record in the shard + TRIM_HORIZON, + # Start reading just after the most recent record in the shard, so that you always read the most recent data in the shard + LATEST, + # Start reading exactly from the position denoted by a specific sequence number + AT_SEQUENCE_NUMBER, + # Start reading right after the position denoted by a specific sequence number + AFTER_SEQUENCE_NUMBER +} diff --git a/ballerina/external_functions.bal b/ballerina/external_functions.bal new file mode 100644 index 0000000..49ae47f --- /dev/null +++ b/ballerina/external_functions.bal @@ -0,0 +1,42 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/jballerina.java; + +isolated function ofEpochSecond(int epochSeconds, int nanoAdjustments) returns handle = @java:Method { + 'class: "java.time.Instant", + name: "ofEpochSecond" +} external; + +isolated function getZoneId(handle zoneId) returns handle = @java:Method { + 'class: "java.time.ZoneId", + name: "of" +} external; + +isolated function atZone(handle receiver, handle zoneId) returns handle = @java:Method { + 'class: "java.time.Instant", + name: "atZone" +} external; + +isolated function ofPattern(handle pattern) returns handle = @java:Method { + 'class: "java.time.format.DateTimeFormatter", + name: "ofPattern" +} external; + +isolated function format(handle receiver, handle dateTimeFormatter) returns handle = @java:Method { + 'class: "java.time.ZonedDateTime", + name: "format" +} external; diff --git a/ballerina/icon.png b/ballerina/icon.png new file mode 100644 index 0000000..9f809ec Binary files /dev/null and b/ballerina/icon.png differ diff --git a/ballerina/records.bal b/ballerina/records.bal new file mode 100644 index 0000000..551aeab --- /dev/null +++ b/ballerina/records.bal @@ -0,0 +1,248 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerinax/'client.config; + +# Represents the AWS DynamoDB Streams Connector configurations. +@display {label: "Connection Config"} +public type ConnectionConfig record {| + *config:ConnectionConfig; + never auth?; + # AWS credentials + AwsCredentials|AwsTemporaryCredentials awsCredentials; + # AWS Region + string region; +|}; + +# Represents AWS credentials. +public type AwsCredentials record { + # AWS access key + string accessKeyId; + # AWS secret key + @display { + label: "", + kind: "password" + } + string secretAccessKey; +}; + +# Represents AWS temporary credentials. +public type AwsTemporaryCredentials record { + # AWS access key + string accessKeyId; + # AWS secret key + @display { + label: "", + kind: "password" + } + string secretAccessKey; + # AWS secret token + @display { + label: "", + kind: "password" + } + string securityToken; +}; + +# Record containing the fields required for describeStream request. +public type DescribeStreamInput record {| + # The Amazon Resource Name (ARN) for the stream + string streamArn; + # The shard ID of the first item that this operation will evaluate. Use the value that was returned for LastEvaluatedShardId in the previous operation + string exclusiveStartShardId?; + # The maximum number of shard objects to return. The upper limit is 100 + int 'limit?; +|}; + +# Represents a single element of a key schema. A key schema specifies the attributes that make up the primary key of a +# table, or the key attributes of an index. +public type KeySchemaElement record { + # The name of a key attribute + string attributeName; + # The role that this key attribute will assume: HASH - partition key, RANGE - sort key + KeyType keyType; +}; + +# Represents all of the data describing a particular stream. +public type StreamDescription record { + # The date and time when the request to create this stream was issued + decimal creationRequestDateTime?; + # The key attribute(s) of the stream's DynamoDB table + KeySchemaElement[] keySchema?; + # The shard ID of the item where the operation stopped, inclusive of the previous result set. Use this value to start a new operation, excluding this value in the new request + string lastEvaluatedShardId?; + # The shards that comprise the stream + Shard[] shards?; + # The Amazon Resource Name (ARN) for the stream + string streamArn?; + # A timestamp, in ISO 8601 format, for this stream + string streamLabel?; + # Indicates the current status of the stream + StreamStatus streamStatus?; + # Indicates the format of the records within this stream + StreamViewType streamViewType?; + # The DynamoDB table with which the stream is associated + string tableName?; +}; + +# A uniquely identified group of stream records within a stream. +public type Shard record { + # The shard ID of the current shard's parent + string parentShardId?; + # The range of possible sequence numbers for the shard + SequenceNumberRange sequenceNumberRange?; + # The system-generated identifier for this shard + string shardId?; +}; + +# The beginning and ending sequence numbers for the stream records contained within a shard. +public type SequenceNumberRange record { + # The last sequence number for the stream records contained within a shard. String contains numeric characters only + string endingSequenceNumber?; + # The first sequence number for the stream records contained within a shard. String contains numeric characters only + string startingSequenceNumber?; +}; + +# Retrieves the stream records from a given shard. +public type GetRecordsInput record {| + # A shard iterator that was retrieved from a previous GetShardIterator operation. This iterator can be used to access the stream records in this shard + string shardIterator; + # The maximum number of records to return from the shard. The upper limit is 1000 + int 'limit?; +|}; + +# Response for the getRecords. +public type GetRecordsOutput record { + # The next position in the shard from which to start sequentially reading stream records. If set to null, the shard has been closed and the requested iterator will not return any more data + string nextShardIterator?; + # The stream records from the shard, which were retrieved using the shard iterator + Record[] records?; +}; + +# A description of a unique event within a stream. +public type Record record { + # The region in which the GetRecords request was received + string awsRegion?; + # The main body of the stream record, containing all of the DynamoDB-specific fields + StreamRecord dynamodb?; + # A globally unique identifier for the event that was recorded in this stream record + string eventID?; + # The type of data modification that was performed on the DynamoDB table + eventName eventName?; + # The AWS service from which the stream record originated. For DynamoDB Streams, this is aws:dynamodb + string eventSource?; + # The version number of the stream record format. This number is updated whenever the structure of Record is modified + string eventVersion?; + # Items that are deleted by the Time to Live process after expiration + Identity userIdentity?; +}; + +# Represents the data for an attribute. Each attribute value is described as a name-value pair. The name is the data +# type, and the value is the data itself. +public type AttributeValue record { + # An attribute of type Binary + string? b? ; + # An attribute of type Boolean + boolean? bool?; + # An attribute of type Binary Set + string[]? bs?; + # An attribute of type List + AttributeValue[]? l?; + # An attribute of type Map + map? m?; + # An attribute of type Number + string? n?; + # An attribute of type Number Set + string[]? ns?; + # An attribute of type Null + boolean? 'null?; + # An attribute of type String + string? s?; + # An attribute of type String Set + string[]? ss?; +}; + +# A description of a single data modification that was performed on an item in a DynamoDB table. +public type StreamRecord record { + # The approximate date and time when the stream record was created, in UNIX epoch time format and rounded down to the closest second + decimal approximateCreationDateTime?; + # The primary key attribute(s) for the DynamoDB item that was modified + AttributeValue keys?; + # The item in the DynamoDB table as it appeared after it was modified + AttributeValue newImage?; + # The item in the DynamoDB table as it appeared before it was modified + AttributeValue oldImage?; + # The sequence number of the stream record + string sequenceNumber?; + # The size of the stream record, in bytes + float sizeBytes?; + # The type of data from the modified DynamoDB item that was captured in this stream record + StreamViewType streamViewType?; +}; + +# Contains details about the type of identity that made the request. +public type Identity record { + # A unique identifier for the entity that made the call. For Time To Live, the principalId is "dynamodb.amazonaws.com" + string principalId; + # The type of the identity. For Time To Live, the type is "Service" + string 'type; +}; + +# The request to perform getShardIterator. +public type GetShardsIteratorInput record {| + # The identifier of the shard. The iterator will be returned for this shard ID + string shardId; + # Determines how the shard iterator is used to start reading stream records from the shard + string shardIteratorType; + # The Amazon Resource Name (ARN) for the stream + string streamArn; + # The sequence number of a stream record in the shard from which to start reading + string sequenceNumber?; +|}; + +# The response of getShardIterator. +public type GetShardsIteratorOutput record { + # The position in the shard from which to start reading stream records sequentially. A shard iterator specifies this position using the sequence number of a stream record in a shard + string shardIterator; +}; + +# Record containing the fields required for listStreams request. +public type ListStreamsInput record {| + # The Amazon Resource Name (ARN) of the first item that this operation will evaluate. Use the value that was returned for LastEvaluatedStreamArn in the previous operation + string exclusiveStartStreamArn?; + # The maximum number of streams to return. The upper limit is 100 + int 'limit?; + # If this parameter is provided, then only the streams associated with this table name are returned + string tableName?; +|}; + +# Response associated with the listStreams request. +public type ListStreamsOutput record { + # The stream ARN of the item where the operation stopped, inclusive of the previous result set. Use this value to start a new operation, excluding this value in the new request + string lastEvaluatedStreamArn?; + # A list of stream descriptors associated with the current account and endpoint + Stream[] streams; +}; + +# Represents all of the data describing a particular stream. +public type Stream record { + # The Amazon Resource Name (ARN) for the stream + string streamArn?; + # A timestamp, in ISO 8601 format, for this stream + string streamLabel?; + # The DynamoDB table with which the stream is associated + string tableName?; +}; diff --git a/ballerina/stream_implementor.bal b/ballerina/stream_implementor.bal new file mode 100644 index 0000000..fc294bb --- /dev/null +++ b/ballerina/stream_implementor.bal @@ -0,0 +1,150 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +class ListStream { + private Stream[] currentEntries = []; + private int index = 0; + private final http:Client httpClient; + private final string accessKeyId; + private final string secretAccessKey; + private final string region; + private final string awsHost; + private final string uri = SLASH; + private string? lastEvaluatedStreamArn; + private ListStreamsInput listStreamInput; + + isolated function init(http:Client httpClient, string host, string accessKey, string secretKey, string region, ListStreamsInput streamInput) + returns error? { + self.httpClient = httpClient; + self.accessKeyId = accessKey; + self.secretAccessKey = secretKey; + self.region = region; + self.awsHost = AWS_STREAMS_SERVICE + DOT + self.region + DOT + AWS_HOST; + self.lastEvaluatedStreamArn = null; + self.listStreamInput = streamInput; + self.currentEntries = check self.fetchStreams(); + } + + public isolated function next() returns record {| Stream value; |}|error? { + if self.index < self.currentEntries.length() { + record {| Stream value; |} 'stream = {value: self.currentEntries[self.index]}; + self.index += 1; + return 'stream; + } + if self.lastEvaluatedStreamArn is string { + self.index = 0; + self.currentEntries = check self.fetchStreams(); + record {| Stream value; |} streamName = {value: self.currentEntries[self.index]}; + self.index += 1; + return streamName; + } + } + + isolated function fetchStreams() returns Stream[]|error { + string target = STREAMS_VERSION + DOT +"ListStreams"; + ListStreamsInput request = { + tableName: self.listStreamInput.tableName, + exclusiveStartStreamArn: self.lastEvaluatedStreamArn, + 'limit: self.listStreamInput.'limit + }; + json payload = check request.cloneWithType(json); + convertJsonKeysToUpperCase(payload); + map signedRequestHeaders = check getSignedRequestHeaders(self.awsHost, self.accessKeyId, + self.secretAccessKey, self.region, + POST, self.uri, target, payload); + json tableListResp = check self.httpClient->post(self.uri, payload, signedRequestHeaders); + convertJsonKeysToCamelCase(tableListResp); + ListStreamsOutput response = check tableListResp.cloneWithType(ListStreamsOutput); + self.lastEvaluatedStreamArn = response?.lastEvaluatedStreamArn; + Stream[]? streamList = response?.streams; + if streamList is Stream[] { + return streamList; + } + return []; + } +} + +class RecordsStream { + private Record[] currentEntries = []; + private int index = 0; + private final http:Client httpClient; + private final string accessKeyId; + private final string secretAccessKey; + private final string region; + private final string awsHost; + private final string uri = SLASH; + private string? nextShardIterator; + private GetRecordsInput getRecordsInput; + + isolated function init(http:Client httpClient, string host, string accessKey, string secretKey, string region, GetRecordsInput getRecordsInput) + returns error? { + self.httpClient = httpClient; + self.accessKeyId = accessKey; + self.secretAccessKey = secretKey; + self.region = region; + self.awsHost = AWS_STREAMS_SERVICE + DOT + self.region + DOT + AWS_HOST; + self.nextShardIterator = null; + self.getRecordsInput = getRecordsInput; + self.currentEntries = check self.fetchRecords(); + } + + public isolated function next() returns record {| Record value; |}|error? { + if self.index < self.currentEntries.length() { + record {| Record value; |} 'record = {value: self.currentEntries[self.index]}; + self.index += 1; + return 'record; + } + if self.nextShardIterator is string { + self.index = 0; + Record[]|error fetchRecordsResult = self.fetchRecords(); + if fetchRecordsResult is Record[] && fetchRecordsResult.length() > 0 { + self.currentEntries = fetchRecordsResult; + } else { + return (); + } + record {| Record value; |} 'record = {value: self.currentEntries[self.index]}; + self.index += 1; + return 'record; + } + } + + isolated function fetchRecords() returns Record[]|error { + string target = STREAMS_VERSION + DOT +"GetRecords"; + GetRecordsInput request = self.getRecordsInput; + json payload = check request.cloneWithType(json); + convertJsonKeysToUpperCase(payload); + map signedRequestHeaders = check getSignedRequestHeaders(self.awsHost, self.accessKeyId, + self.secretAccessKey, self.region, + POST, self.uri, target, payload); + json response = check self.httpClient->post(self.uri, payload, signedRequestHeaders); + convertJsonKeysToCamelCase(response); + GetRecordsOutput records = check response.cloneWithType(GetRecordsOutput); + self.nextShardIterator = records?.nextShardIterator; + if self.nextShardIterator is string { + self.getRecordsInput.shardIterator = self.nextShardIterator; + } + Record[]? recordList = records?.'records; + if recordList is Record[] && recordList.length() > 0 { + return recordList; + } else if records.hasKey("nextShardIterator") && self.nextShardIterator is string { + return self.fetchRecords(); + } + return []; + } +} + diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal new file mode 100644 index 0000000..91ee4cc --- /dev/null +++ b/ballerina/tests/test.bal @@ -0,0 +1,148 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; +import ballerina/os; +import ballerinax/aws.dynamodb; + +configurable string accessKeyId = os:getEnv("ACCESS_KEY_ID"); +configurable string secretAccessKey = os:getEnv("SECRET_ACCESS_KEY"); +configurable string region = os:getEnv("REGION"); + +final string mainTable = "TestStreamTable"; +final string streamArn = "arn:aws:dynamodb:us-east-1:134633749276:table/TestStreamTable/stream/2024-01-04T04:43:13.919"; +final dynamodb:Client dynamodbClient = check new(config); + +ConnectionConfig config = { + awsCredentials: {accessKeyId: accessKeyId, secretAccessKey: secretAccessKey}, + region: region +}; + +Client dynamoDBStreamClient = check new (config); + +@test:BeforeSuite +function updateItem() returns error? { + dynamodb:ItemCreateInput request = { + tableName: mainTable, + item: { + "LastPostDateTime": { + "S": "201303190422" + }, + "Tags": { + "SS": [ + "Update", + "Multiple Items", + "HelpMe" + ] + }, + "ForumName": { + "S": "Amazon DynamoDB" + }, + "Message": { + "S": "I want to update multiple items in a single call. What's the best way to do that?" + }, + "Subject": { + "S": "How do I update multiple items?" + }, + "LastPostedBy": { + "S": "fred@example.com" + } + }, + conditionExpression: "ForumName <> :f and Subject <> :s", + returnValues: dynamodb:ALL_OLD, + returnItemCollectionMetrics: dynamodb:SIZE, + returnConsumedCapacity: dynamodb:TOTAL, + expressionAttributeValues: { + ":f": { + "S": "Amazon DynamoDB" + }, + ":s": { + "S": "How do I update multiple items?" + } + } + }; + _ = check dynamodbClient->createItem(request); +} + +@test:Config{} +function testStreamsList() returns error? { + ListStreamsInput listStreamsInput = { + tableName: mainTable, + 'limit: 1 + }; + stream response = check dynamoDBStreamClient->listStreams(listStreamsInput); + check response.forEach(function(Stream resp) { + test:assertEquals(resp.tableName, mainTable); + }); +} + +@test:Config{} +function testDescribeStreams() returns error? { + DescribeStreamInput describeStream = { + streamArn: streamArn + }; + StreamDescription response = check dynamoDBStreamClient->describeStream(describeStream); + test:assertEquals(response.tableName, mainTable); + test:assertEquals(response.streamStatus, "ENABLED"); +} + +@test:Config{} +function testGetRecords() returns error? { + DescribeStreamInput describeStream = { + streamArn: streamArn + }; + StreamDescription response = check dynamoDBStreamClient->describeStream(describeStream); + test:assertEquals(response.tableName, mainTable); + string shardId = ""; + Shard[]? shards = response.shards; + test:assertTrue(shards is Shard[]); + if shards is Shard[] { + shardId = shards[0].shardId; + } + GetShardsIteratorInput shardIteratorReq = { + shardIteratorType: TRIM_HORIZON, + shardId: shardId, + streamArn: streamArn + }; + GetShardsIteratorOutput shardIterator = check dynamoDBStreamClient->getShardIterator(shardIteratorReq); + test:assertFalse(shardIterator.shardIterator is ""); + GetRecordsInput getRecordsInput = { + shardIterator: shardIterator.shardIterator + }; + stream streamResult = check dynamoDBStreamClient->getRecords(getRecordsInput); + check streamResult.forEach(function(Record srecord) { + test:assertEquals(srecord.eventSource, "aws:dynamodb"); + }); +} + +@test:AfterSuite +function deleteUpdatedItem() returns error? { + dynamodb:ItemDeleteInput delRequest = { + tableName: mainTable, + 'key: { + "ForumName": { + "S": "Amazon DynamoDB" + }, + "Subject": { + "S": "How do I update multiple items?" + } + }, + returnConsumedCapacity: dynamodb:TOTAL, + returnItemCollectionMetrics: dynamodb:SIZE, + returnValues: dynamodb:ALL_OLD + }; + _= check dynamodbClient->deleteItem(delRequest); +} diff --git a/ballerina/utils.bal b/ballerina/utils.bal new file mode 100644 index 0000000..da6c08d --- /dev/null +++ b/ballerina/utils.bal @@ -0,0 +1,205 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/crypto; +import ballerina/jballerina.java; +import ballerina/lang.array; +import ballerina/time; +import ballerina/url; + +isolated map requestDataBindingMap = { + "sseSpecification": "SSESpecification", + "kmsMasterKeyId": "KMSMasterKeyId", + "kmsMasterKeyArn": "KMSMasterKeyArn", + "sseDescription": "SSEDescription", + "bool": "BOOL", + "bs": "BS", + "ns": "NS", + "null": "NULL", + "ss": "SS" + }; + +isolated map responseDataBindingMap = { + "SSESpecification": "sseSpecification", + "KMSMasterKeyId": "kmsMasterKeyId", + "SSEType": "sseType", + "KMSMasterKeyArn": "kmsMasterKeyArn", + "SSEDescription": "sseDescription", + "BOOL": "bool", + "BS": "bs", + "NS": "ns", + "NULL": "null", + "SS": "ss" + }; +isolated function getSignedRequestHeaders(string host, string accessKey, string secretKey, string region, string verb, + string uri, string amzTarget, json requestPayload) returns map|error { + + string content_type =APPLICATION_JSON; + string canonicalUri = check getCanonicalURI(uri); + string payload = requestPayload === EMPTY_STRING ? EMPTY_STRING : requestPayload.toJsonString(); + + [int, decimal] & readonly currentTime = time:utcNow(); + string|error amzDate = utcToString(currentTime, ISO8601_BASIC_DATE_FORMAT); + string|error dateStamp = utcToString(currentTime, SHORT_DATE_FORMAT); + + if amzDate is string && dateStamp is string { + string canonicalQuerystring = EMPTY_STRING; + + string canonicalHeaders = CONTENT_TYPE + COLON + content_type + NEW_LINE +HOST + COLON + host + NEW_LINE + + X_AMZ_DATE + COLON + amzDate + NEW_LINE + X_AMZ_TARGET + COLON + amzTarget + NEW_LINE; + + string signedHeaders = CONTENT_TYPE + SEMICOLON + HOST + SEMICOLON + X_AMZ_DATE + SEMICOLON + X_AMZ_TARGET; + + string payloadHash = array:toBase16(crypto:hashSha256(payload.toBytes())).toLowerAscii(); + + string canonicalRequest = verb + NEW_LINE + canonicalUri + NEW_LINE + canonicalQuerystring + NEW_LINE + + canonicalHeaders + NEW_LINE + signedHeaders + NEW_LINE + payloadHash; + + string credentialScope = dateStamp + SLASH + region + SLASH + AWS_SERVICE + + SLASH + AWS4_REQUEST; + + string stringToSign = AWS4_HMAC_SHA256 + NEW_LINE + amzDate + NEW_LINE + credentialScope + NEW_LINE + + array:toBase16(crypto:hashSha256(canonicalRequest.toBytes())).toLowerAscii(); + + byte[] signingKey = check getSignatureKey(secretKey, dateStamp, region, AWS_SERVICE); + + string signature = array:toBase16(check crypto:hmacSha256(stringToSign.toBytes(), signingKey)).toLowerAscii(); + + string authorizationHeader = AWS4_HMAC_SHA256 + SPACE + CREDENTIAL + EQUAL + accessKey + SLASH + + credentialScope + COMMA + SPACE + SIGNED_HEADER + EQUAL + signedHeaders + + COMMA + SPACE + SIGNATURE + EQUAL + signature; + + map headers = {}; + headers[HEADER_CONTENT_TYPE] = content_type; + headers[HEADER_HOST] = host; + headers[HEADER_X_AMZ_DATE] = amzDate; + headers[HEADER_X_AMZ_TARGET] = amzTarget; + headers[HEADER_AUTHORIZATION] = authorizationHeader; + + return headers; + } else { + if amzDate is error { + return error(GENERATE_SIGNED_REQUEST_HEADERS_FAILED_MSG, amzDate); + } else if dateStamp is error { + return error (GENERATE_SIGNED_REQUEST_HEADERS_FAILED_MSG, dateStamp); + } else { + return error (GENERATE_SIGNED_REQUEST_HEADERS_FAILED_MSG); + } + } +} + +isolated function sign(byte[] key, string msg) returns byte[]|error { + return check crypto:hmacSha256(msg.toBytes(), key); +} + +isolated function getSignatureKey(string secretKey, string datestamp, string region, string serviceName) + returns byte[]|error { + string awskey = (AWS4 + secretKey); + byte[] kDate = check sign(awskey.toBytes(), datestamp); + byte[] kRegion = check sign(kDate, region); + byte[] kService = check sign(kRegion, serviceName); + byte[] kSigning = check sign(kService, AWS4_REQUEST); + return kSigning; +} + +isolated function utcToString(time:Utc utc, string pattern) returns string|error { + [int, decimal][epochSeconds, lastSecondFraction] = utc; + int nanoAdjustments = (lastSecondFraction * 1000000000); + var instant = ofEpochSecond(epochSeconds, nanoAdjustments); + var zoneId = getZoneId(java:fromString(Z)); + var zonedDateTime = atZone(instant, zoneId); + var dateTimeFormatter = ofPattern(java:fromString(pattern)); + handle formatString = format(zonedDateTime, dateTimeFormatter); + return formatString.toBalString(); +} + +isolated function getCanonicalURI(string requestURI) returns string|error { + string value = check url:encode(requestURI, UTF_8); + return re `%2F`.replaceAll(value, SLASH, 0); +} + +isolated function convertJsonKeysToCamelCase(json req) { + map mapValue = >req; + foreach var [key, value] in mapValue.entries() { + string converted = lowercaseFirstLetter(key); + if converted != key { + any|error removeResult = mapValue.remove(key); + mapValue[converted] = value; + } + if value is json[] { + json[] innerJson = mapValue[converted]; + foreach var item in innerJson { + // assume no arrays inside array + if item is map { + convertJsonKeysToCamelCase(item); + } + } + } else if value is map { + convertJsonKeysToCamelCase(value); + } + } +} + +function convertJsonArrayToCamelCase(json[] jsonArr) { + foreach var item in jsonArr { + convertJsonKeysToCamelCase(item); + } +} + +isolated function convertJsonKeysToUpperCase(json req) { + map mapValue = >req; + foreach var [key, value] in mapValue.entries() { + string converted = uppercaseFirstLetter(key); + if converted != key { + any|error removeResult = mapValue.remove(key); + mapValue[converted] = value; + } + if value is json[] { + json[] innerJson = mapValue[converted]; + foreach var item in innerJson { + // assume no arrays inside array + if item is map { + convertJsonKeysToUpperCase(item); + } + } + } else if value is map { + convertJsonKeysToUpperCase(value); + } + } +} + +isolated function uppercaseFirstLetter(string str) returns string { + lock { + if requestDataBindingMap.hasKey(str) { + return requestDataBindingMap[str]; + } + } + string firstLetter = str.substring(0, 1); + string remainingLetters = str.substring(1); + return firstLetter.toUpperAscii() + remainingLetters; +} + +isolated function lowercaseFirstLetter(string str) returns string { + lock { + if responseDataBindingMap.hasKey(str) { + return responseDataBindingMap[str]; + } + } + string firstLetter = str.substring(0, 1); + string remainingLetters = str.substring(1); + return firstLetter.toLowerAscii() + remainingLetters; +} + diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml new file mode 100644 index 0000000..bdc8b76 --- /dev/null +++ b/build-config/resources/Ballerina.toml @@ -0,0 +1,12 @@ +[package] +distribution = "2201.8.0" +org = "ballerinax" +name = "aws.dynamodbstreams" +version = "@toml.version@" +authors = ["Ballerina"] +repository = "https://github.com/ballerina-platform/module-ballerinax-aws.dynamodbstreams" +icon = "icon.png" +license = ["Apache-2.0"] + +[build-options] +observabilityIncluded = true diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..8b0b6b7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'com.github.spotbugs-base' + id 'com.github.johnrengelman.shadow' + id 'de.undercouch.download' + id 'net.researchgate.release' +} + +ext.ballerinaLangVersion = project.ballerinaLangVersion + +allprojects { + group = project.group + version = project.version + + apply plugin: 'maven-publish' + + repositories { + mavenLocal() + maven { + url = 'https://maven.wso2.org/nexus/content/repositories/releases/' + } + + maven { + url = 'https://maven.wso2.org/nexus/content/groups/wso2-public/' + } + + maven { + url = 'https://repo.maven.apache.org/maven2' + } + + maven { + url = 'https://maven.pkg.github.com/ballerina-platform/*' + credentials { + username System.getenv("packageUser") + password System.getenv("packagePAT") + } + } + } + + ext { + snapshotVersion = '-SNAPSHOT' + timestampedVersionRegex = '.*-\\d{8}-\\d{6}-\\w.*\$' + } +} + +def moduleVersion = project.version.replace("-SNAPSHOT", "") + +task build { + dependsOn(':aws.dynamodbstreams-ballerina:build') + dependsOn(':aws.dynamodbstreams-examples:build') +} + +release { + buildTasks = ['build'] + failOnSnapshotDependencies = true + versionPropertyFile = 'gradle.properties' + tagTemplate = 'v${version}' + git { + requireBranch = "release-${moduleVersion}" + pushToRemote = 'origin' + } +} diff --git a/docs/spec/spec.md b/docs/spec/spec.md new file mode 100644 index 0000000..34ceadf --- /dev/null +++ b/docs/spec/spec.md @@ -0,0 +1,115 @@ +# Specification: Ballerina DynamoDB Streams Library + +_Owners_: @bhashinee +_Reviewers_: @daneshk +_Created_: 2023/11/09 +_Updated_: 2024/01/04 +_Edition_: Swan Lake + +## Introduction + +This is the specification for the DynamoDB streams connector of [Ballerina language](https://ballerina.io/), which allows you to access the Amazon DynamoDB REST API. + +The DynamoDB streams connector specification has evolved and may continue to evolve in the future. The released versions of the specification can be found under the relevant GitHub tag. + +If you have any feedback or suggestions about the library, start a discussion via a [GitHub issue](https://github.com/ballerina-platform/ballerina-standard-library/issues) or in the [Discord server](https://discord.gg/ballerinalang). Based on the outcome of the discussion, the specification and implementation can be updated. Community feedback is always welcome. Any accepted proposal, which affects the specification is stored under `/docs/proposals`. Proposals under discussion can be found with the label `type/proposal` in GitHub. + +The conforming implementation of the specification is released and included in the distribution. Any deviation from the specification is considered a bug. + +## Contents +1. [Overview](#1-overview) +2. [Client](#2-client) + 1. [Client Configurations](#21-client-configurations) + 2. [Initialization](#22-initialization) + 3. [APIs](#23-apis) + 1. [listStreams](#listStreams) + 2. [describeStream](#describeStream) + 3. [getShardIterator](#getShardIterator) + 4. [getRecords](#getRecords) + +## 1. [Overview](#1-overview) + +The Ballerina `dynamodbstreams` library facilitates APIs to allow you to access the Amazon DynamoDB REST API specifically to work with DynamoDB streams. +DynamoDB Streams is a feature provided by Amazon DynamoDB, a fully managed NoSQL database service. DynamoDB Streams enables you to capture and track changes made to items in a DynamoDB table in real-time. It's a mechanism for creating a time-ordered sequence of item-level modifications within a table. + +## 2. [Client](#2-client) + +`dynamodbstreams:Client` can be used to access the Amazon DynamoDB Streams REST API. + +### 2.1. [Client Configurations](#21-client-configurations) + +When initializing the client, following configurations can be provided, + +```ballerina +public type ConnectionConfig record {| + *config:ConnectionConfig; + never auth?; + # AWS credentials + AwsCredentials|AwsTemporaryCredentials awsCredentials; + # AWS Region + string region; +|}; +``` + +### 2.2. [Initialization](#22-initialization) + +A client can be initialized by providing the AwsCredentials and optionally the other configurations in `ClientConfiguration`. + +```ballerina +ConnectionConfig config = { + awsCredentials: {accessKeyId: "ACCESS_KEY_ID", secretAccessKey: "SECRET_ACCESS_KEY"}, + region: "ap-south-1" +}; + +Client dynamoDBClient = check new (config); +``` + +### 2.3 [APIs](#23-apis) + +#### [listStreams](#listStreams) + +This API can be used to list streams in DynamoDB account. + +```ballerina +# Returns an array of stream ARNs associated with the current account and endpoint. If the TableName parameter is present, then ListStreams will return only the streams ARNs for that table. +# +# + request - The required details to list the streams +# + return - If success, `stream` or else an error +remote isolated function listStreams(ListStreamsInput request) returns stream|error { +``` + +#### [describeStream](#describeStream) + +This API can be used to get information about a stream. + +```ballerina +# Returns information about a stream, including the current status of the stream, its Amazon Resource Name (ARN), the composition of its shards, and its corresponding DynamoDB table. +# +# + request - The required details to describe the stream +# + return - If success, dynamodb:StreamDescription record, else an error +remote isolated function describeStream(DescribeStreamInput request) returns StreamDescription|error { +``` + +#### [getShardIterator](#getShardIterator) + +This API can be used to get a shard iterator. + +```ballerina +# Returns a shard iterator. A shard iterator provides information about how to retrieve the stream records from within a shard. Use the shard iterator in a subsequent GetRecords request to read the stream records from the shard. +# +# + request - The required details to get the shard iterator +# + return - If success, ShardIterator, else an error +remote isolated function getShardIterator(GetShardsIteratorInput request) returns GetShardsIteratorOutput|error { +``` + +#### [getRecords](#getRecords) + +This API can be used to get the stream records from a given shard. + +```ballerina +# Retrieves the stream records from a given shard. +# +# + request - The required details to get the records from a shard +# + return - If success, ShardIterator, else an error +remote isolated function getRecords(GetRecordsInput request) returns stream|error { +``` diff --git a/examples/build.gradle b/examples/build.gradle new file mode 100644 index 0000000..1e7d255 --- /dev/null +++ b/examples/build.gradle @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.tools.ant.taskdefs.condition.Os + +apply plugin: 'java' + +def graalvmFlag = "" + +task testExamples { + if (project.hasProperty("balGraalVMTest")) { + graalvmFlag = "--graalvm" + } + doLast { + try { + exec { + workingDir project.projectDir + println("Working dir: ${workingDir}") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'sh', "/c", "chmod +x ./build.sh && ./build.sh run && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', "-c", "chmod +x ./build.sh && ./build.sh run" + } + } + } catch (Exception e) { + println("Example Build failed: " + e.message) + throw e + } + } +} + +task buildExamples { + gradle.taskGraph.whenReady { graph -> + if (graph.hasTask(":dynamodbstreams-examples:test")) { + buildExamples.enabled = false + } else { + testExamples.enabled = false + } + } + doLast { + try { + exec { + workingDir project.projectDir + println("Working dir: ${workingDir}") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'sh', "/c", "chmod +x ./build.s && ./build.sh build && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', "-c", "chmod +x ./build.sh && ./build.sh build" + } + } + } catch (Exception e) { + println("Example Build failed: " + e.message) + throw e + } + } +} + +buildExamples.dependsOn ":aws.dynamodbstreams-ballerina:build" +testExamples.dependsOn ":aws.dynamodbstreams-ballerina:build" +test.dependsOn testExamples +build.dependsOn buildExamples diff --git a/examples/build.sh b/examples/build.sh new file mode 100755 index 0000000..8be8954 --- /dev/null +++ b/examples/build.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +BAL_EXAMPLES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BAL_CENTRAL_DIR="$HOME/.ballerina/repositories/central.ballerina.io/" +BAL_HOME_DIR="$BAL_EXAMPLES_DIR/../ballerina" + +set -e + +case "$1" in +build) + BAL_CMD="build" + ;; +run) + BAL_CMD="run" + ;; +*) + echo "Invalid command provided: '$1'. Please provide 'build' or 'run' as the command." + exit 1 + ;; +esac + +# Read Ballerina package name +BAL_PACKAGE_NAME=$(awk -F'"' '/^name/ {print $2}' "$BAL_HOME_DIR/Ballerina.toml") + +# Push the package to the local repository +cd "$BAL_HOME_DIR" && + bal pack && + bal push --repository=local + +# Remove the cache directories in the repositories +cacheDirs=($(ls -d "$BAL_CENTRAL_DIR"/cache-* 2>/dev/null)) +for dir in "${cacheDirs[@]}"; do + [ -d "$dir" ] && rm -r "$dir" +done +echo "Successfully cleaned the cache directories" + +# Update the central repository +BAL_DESTINATION_DIR="$HOME/.ballerina/repositories/central.ballerina.io/bala/ballerinax/$BAL_PACKAGE_NAME" +BAL_SOURCE_DIR="$HOME/.ballerina/repositories/local/bala/ballerinax/$BAL_PACKAGE_NAME" +[ -d "$BAL_DESTINATION_DIR" ] && rm -r "$BAL_DESTINATION_DIR" +[ -d "$BAL_SOURCE_DIR" ] && cp -r "$BAL_SOURCE_DIR" "$BAL_DESTINATION_DIR" +echo "Successfully updated the local central repositories" + +echo "$BAL_DESTINATION_DIR" +echo "$BAL_SOURCE_DIR" + +# Loop through examples in the examples directory +find "$BAL_EXAMPLES_DIR" -type f -name "*.bal" | while read -r BAL_EXAMPLE_FILE; do + bal "$BAL_CMD" --offline "$BAL_EXAMPLE_FILE" +done + +# Remove generated JAR files +find "$BAL_HOME_DIR" -maxdepth 1 -type f -name "*.jar" | while read -r JAR_FILE; do + rm "$JAR_FILE" +done diff --git a/examples/order-management/README.md b/examples/order-management/README.md new file mode 100644 index 0000000..8f06422 --- /dev/null +++ b/examples/order-management/README.md @@ -0,0 +1,26 @@ +# Real-time Order Processing + +## Overview + +Consider a scenario you want to do real-time order processing. You can simply do this by using DynamoDB streams. In the context of DynamoDB Streams, you can use DynamoDB Streams to capture changes to your order data and react to those changes in near real-time. This example demonstrates how the basic operations of real-time order processing actions can be done using Ballerina DynamoDB streams API. + +## Implementation + +1. Assume you have a DynamoDB table named 'Orders' with the following schema: + a. Partition key: OrderId (String) + +2. You can log into the AWS account and enable the streams. + +3. Polling to get the new orders and order updates. + +In this example, we'll use a basic long-polling approach to continuously check the DynamoDB Stream for new records. You can use the `getRecords` API for this. + +## Run the Example + +First, clone this repository, and then run the following commands to run this example in your local machine. + +```sh +// Run the dynamoDB client +$ cd examples/game/client +$ bal run +``` diff --git a/examples/order-management/client.bal b/examples/order-management/client.bal new file mode 100644 index 0000000..5ce1579 --- /dev/null +++ b/examples/order-management/client.bal @@ -0,0 +1,59 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/os; +import ballerinax/aws.dynamodbstreams; + +public function main() returns error? { + dynamodbstreams:ConnectionConfig amazonDynamodbConfig = { + awsCredentials: { + accessKeyId: os:getEnv("ACCESS_KEY_ID"), + secretAccessKey: os:getEnv("SECRET_ACCESS_KEY") + }, + region: os:getEnv("REGION") + }; + + dynamodbstreams:Client dynamodbStreamsClient = check new(amazonDynamodbConfig); + + dynamodbstreams:DescribeStreamInput describeStream = { + streamArn: "arn:aws:dynamodb:us-east-1:134633749276:table/TestTable/stream/2023-11-17T09:36:43.853" + }; + + dynamodbstreams:StreamDescription describeStreamResult = check dynamodbStreamsClient->describeStream(describeStream); + string shardId = ""; + dynamodbstreams:Shard[]? shards = describeStreamResult.shards; + + if shards is dynamodbstreams:Shard[] { + shardId = shards[0].shardId; + } + + dynamodbstreams:GetShardsIteratorInput shardIteratorReq = { + shardIteratorType: dynamodbstreams:TRIM_HORIZON, + shardId: shardId, + streamArn: "arn:aws:dynamodb:us-east-1:134633749276:table/TestTable/stream/2023-11-17T09:36:43.853" + }; + dynamodbstreams:GetShardsIteratorOutput shardIterator = check dynamodbStreamsClient->getShardIterator(shardIteratorReq); + dynamodbstreams:GetRecordsInput getRecordsInput = { + shardIterator: shardIterator.shardIterator + }; + while true { + stream result = check dynamodbStreamsClient->getRecords(getRecordsInput); + check result.forEach(function(dynamodbstreams:Record srecord) { + io:println(result); + }); + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..82a8586 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +org.gradle.caching=true +group=io.ballerina.stdlib +version=1.0.0-SNAPSHOT + +ballerinaLangVersion=2201.8.0 +checkstylePluginVersion=10.12.0 +spotbugsPluginVersion=5.0.14 +shadowJarPluginVersion=8.1.1 +downloadPluginVersion=5.4.0 +releasePluginVersion=2.8.0 +testngVersion=7.6.1 +eclipseLsp4jVersion=0.12.0 +ballerinaGradlePluginVersion=2.1.6 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..033e24c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9f4197d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..fcb6fca --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..6689b85 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..608fdaf --- /dev/null +++ b/settings.gradle @@ -0,0 +1,46 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.3/userguide/building_swift_projects.html in the Gradle documentation. + */ + +pluginManagement { + plugins { + id "com.github.spotbugs-base" version "${spotbugsPluginVersion}" + id "com.github.johnrengelman.shadow" version "${shadowJarPluginVersion}" + id "de.undercouch.download" version "${downloadPluginVersion}" + id "net.researchgate.release" version "${releasePluginVersion}" + id "io.ballerina.plugin" version "${ballerinaGradlePluginVersion}" + } + + repositories { + gradlePluginPortal() + maven { + url = 'https://maven.pkg.github.com/ballerina-platform/*' + credentials { + username System.getenv("packageUser") + password System.getenv("packagePAT") + } + } + } +} + +plugins { + id "com.gradle.enterprise" version "3.2" +} + +rootProject.name = 'module-ballerinax-aws.dynamodbstreams' + +include ':aws.dynamodbstreams-ballerina' +include ':aws.dynamodbstreams-examples' + +project(':aws.dynamodbstreams-ballerina').projectDir = file("ballerina") +project(':aws.dynamodbstreams-examples').projectDir = file("examples") + +gradleEnterprise { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +}