diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6584849 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# These are Windows java files and should use lf +*.java text eol=lf diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..35887fc --- /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 diff --git a/.github/workflows/build-timestamped-master.yml b/.github/workflows/build-timestamped-master.yml new file mode 100644 index 0000000..6442123 --- /dev/null +++ b/.github/workflows/build-timestamped-master.yml @@ -0,0 +1,18 @@ +name: Build + +on: + push: + branches: + - main + paths-ignore: + - '*.md' + - 'docs/**' + + workflow_dispatch: + +jobs: + call_workflow: + name: Run Build Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-library/.github/workflows/build-timestamp-master-template.yml@main + secrets: inherit 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..1cf890e --- /dev/null +++ b/.github/workflows/build-with-bal-test-graalvm.yml @@ -0,0 +1,41 @@ +name: GraalVM Check + +on: + workflow_dispatch: + inputs: + lang_tag: + description: Branch/Release Tag of the Ballerina Lang + required: true + default: master + lang_version: + description: Ballerina Lang Version (If given ballerina lang buid will be skipped) + required: false + default: '' + native_image_options: + description: Default native-image options + required: false + default: '' + schedule: + - cron: '30 18 * * *' + pull_request: + branches: + - main + types: [ opened, synchronize, reopened, labeled, unlabeled ] + +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-library/.github/workflows/build-with-bal-test-graalvm-template.yml@main + with: + lang_tag: ${{ inputs.lang_tag }} + lang_version: ${{ inputs.lang_version }} + native_image_options: '-J-Xmx7G ${{ inputs.native_image_options }}' + additional_windows_build_flags: '-x test' + steps: + - name: Give execute permission to gradlew + run: chmod +x ./gradlew diff --git a/.github/workflows/central-publish.yml b/.github/workflows/central-publish.yml new file mode 100644 index 0000000..11922b5 --- /dev/null +++ b/.github/workflows/central-publish.yml @@ -0,0 +1,21 @@ +name: Publish to the Ballerina central + +on: + workflow_dispatch: + inputs: + environment: + type: choice + description: Select Environment + required: true + options: + - DEV CENTRAL + - STAGE CENTRAL + +jobs: + call_workflow: + name: Run Central Publish Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-library/.github/workflows/central-publish-template.yml@main + secrets: inherit + with: + environment: ${{ github.event.inputs.environment }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..cc206bb --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,78 @@ +name: Publish release + +on: + workflow_dispatch: + repository_dispatch: + types: [ stdlib-release-pipeline ] + +jobs: + publish-release: + runs-on: ubuntu-latest + if: github.repository_owner == 'ballerina-platform' + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: 17.0.7 + - name: Give execute permission to gradlew + run: chmod +x ./gradlew + - name: Build with Gradle + env: + packageUser: ${{ github.actor }} + packagePAT: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global user.name ${{ secrets.BALLERINA_BOT_USERNAME }} + git config --global user.email ${{ secrets.BALLERINA_BOT_EMAIL }} + ./gradlew build -x check -x test + - name: Create lib directory if not exists + run: mkdir -p ballerina/lib + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'rootfs' + scan-ref: '/github/workspace/ballerina/lib' + format: 'table' + timeout: '10m0s' + exit-code: '1' + - name: Set version env variable + run: echo "VERSION=$((grep -w 'version' | cut -d= -f2) < gradle.properties | rev | cut --complement -d- -f1 | rev)" >> $GITHUB_ENV + - name: Pre release dependency version update + env: + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + run: | + echo "Version: ${VERSION}" + git checkout -b release-${VERSION} + sed -i 's/ballerinaLangVersion=\(.*\)-SNAPSHOT/ballerinaLangVersion=\1/g' gradle.properties + sed -i 's/ballerinaLangVersion=\(.*\)-[0-9]\{8\}-[0-9]\{6\}-.*$/ballerinaLangVersion=\1/g' gradle.properties + sed -i 's/stdlib\(.*\)=\(.*\)-SNAPSHOT/stdlib\1=\2/g' gradle.properties + sed -i 's/stdlib\(.*\)=\(.*\)-[0-9]\{8\}-[0-9]\{6\}-.*$/stdlib\1=\2/g' gradle.properties + sed -i 's/observe\(.*\)=\(.*\)-SNAPSHOT/observe\1=\2/g' gradle.properties + sed -i 's/observe\(.*\)=\(.*\)-[0-9]\{8\}-[0-9]\{6\}-.*$/observe\1=\2/g' gradle.properties + git add gradle.properties + git commit -m "Move dependencies to stable version" || echo "No changes to commit" + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Publish artifact + env: + BALLERINA_CENTRAL_ACCESS_TOKEN: ${{ secrets.BALLERINA_CENTRAL_ACCESS_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + publishUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + publishPAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + nexusUser: ${{ secrets.NEXUS_USERNAME }} + nexusPassword: ${{ secrets.NEXUS_PASSWORD }} + CLIENT_ID: ${{ secrets.CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} + REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }} + run: | + ./gradlew clean release -Prelease.useAutomaticVersion=true + ./gradlew -Pversion=${VERSION} publish -x test -PpublishToCentral=true + - name: GitHub Release and Release Sync PR + env: + GITHUB_TOKEN: ${{ secrets.BALLERINA_BOT_TOKEN }} + run: | + gh release create v$VERSION --title "module-ballerinax-persist.redis-v$VERSION" + gh pr create --title "[Automated] Sync main after $VERSION release" --body "Sync main after $VERSION release" diff --git a/.github/workflows/publish-snapshot-nexus.yml b/.github/workflows/publish-snapshot-nexus.yml new file mode 100644 index 0000000..4672737 --- /dev/null +++ b/.github/workflows/publish-snapshot-nexus.yml @@ -0,0 +1,29 @@ +name: Publish Snapshot to Nexus + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + if: github.repository_owner == 'ballerina-platform' + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: 17.0.7 + - name: Give execute permission to gradlew + run: chmod +x ./gradlew + - name: Build with Gradle + env: + packageUser: ${{ secrets.BALLERINA_BOT_USERNAME }} + packagePAT: ${{ secrets.BALLERINA_BOT_TOKEN }} + nexusUser: ${{ secrets.NEXUS_USERNAME }} + nexusPassword: ${{ secrets.NEXUS_PASSWORD }} + CLIENT_ID: ${{ secrets.CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} + REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }} + run: | + ./gradlew build publishMavenJavaPublicationToWSO2NexusRepository --scan --no-daemon diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..19a104e --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,17 @@ +name: PR Build + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + call_workflow: + name: Run PR Build Workflow + if: ${{ github.repository_owner == 'ballerina-platform' }} + uses: ballerina-platform/ballerina-library/.github/workflows/pull-request-build-template.yml@main + with: + additional-windows-test-flags: "-x test" diff --git a/.github/workflows/stale_check.yml b/.github/workflows/stale_check.yml new file mode 100644 index 0000000..8763360 --- /dev/null +++ b/.github/workflows/stale_check.yml @@ -0,0 +1,19 @@ +name: 'Close stale pull requests' + +on: + schedule: + - cron: '30 19 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + stale-pr-message: 'This PR has been open for more than 15 days with no activity. This will be closed in 3 days unless the `stale` label is removed or commented.' + close-pr-message: 'Closed PR due to inactivity for more than 18 days.' + days-before-pr-stale: 15 + days-before-pr-close: 3 + days-before-issue-stale: -1 + days-before-issue-close: -1 diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 0000000..458aab5 --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,13 @@ +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-library/.github/workflows/trivy-scan-template.yml@main + secrets: inherit diff --git a/.gitignore b/.gitignore index 524f096..78eabe3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,29 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Ballerina +Ballerina.lock + +# VS code files +.vscode + +# IDEA code files +.idea + +# Generated files +target +.ballerina +ballerina/target +ballerina/.devcontainer.json +ballerina/build + +# gradle +.gradle +build/ +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache + +# Ignore Gradle build output directory +build diff --git a/ballerina/.devcontainer.json b/ballerina/.devcontainer.json new file mode 100644 index 0000000..e058e4f --- /dev/null +++ b/ballerina/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "image": "ballerina/ballerina-devcontainer:2201.8.2", + "extensions": ["WSO2.ballerina"], +} diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml new file mode 100644 index 0000000..3656646 --- /dev/null +++ b/ballerina/Ballerina.toml @@ -0,0 +1,24 @@ +[package] +org = "ballerinax" +name = "persist.redis" +version = "0.1.0" +authors = ["Ballerina"] +keywords = ["persist", "redis", "experimental"] +repository = "https://github.com/ballerina-platform/module-ballerinax-persist.redis" +license = ["Apache-2.0"] +distribution = "2201.9.0" + +[platform.java17] +graalvmCompatible = true + +[[platform.java17.dependency]] +groupId = "io.ballerina.persist" +artifactId = "persist-redis-native" +version = "0.1.0" +path = "../native/build/libs/persist.redis-native-0.1.0-SNAPSHOT.jar" + +[[platform.java17.dependency]] +groupId = "io.ballerina.stdlib" +artifactId = "persist-native" +version = "1.2.0" +path = "./lib/persist-native-1.2.0.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml new file mode 100644 index 0000000..8989628 --- /dev/null +++ b/ballerina/Dependencies.toml @@ -0,0 +1,176 @@ +# 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.9.0-20240229-103900-a949e6d4" + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.6.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[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 = "lang.__internal" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.error" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" +scope = "testOnly" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "lang.regexp", moduleName = "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"} +] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.2.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "persist" +version = "1.2.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "persist", moduleName = "persist"} +] + +[[package]] +org = "ballerina" +name = "test" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {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 = "ballerinax" +name = "persist.redis" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "persist"}, + {org = "ballerina", name = "test"}, + {org = "ballerina", name = "time"}, + {org = "ballerinax", name = "redis"} +] +modules = [ + {org = "ballerinax", packageName = "persist.redis", moduleName = "persist.redis"} +] + +[[package]] +org = "ballerinax" +name = "redis" +version = "3.0.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerinax", packageName = "redis", moduleName = "redis"} +] + diff --git a/ballerina/Module.md b/ballerina/Module.md new file mode 100644 index 0000000..22b0063 --- /dev/null +++ b/ballerina/Module.md @@ -0,0 +1,51 @@ +# Overview + +This module provide Redis database support for the `bal persist` feature, which provides functionality to store and query data from a +Redis database through a data model instead of writing Redis commands. + +Since Redis is not the default datastore for `bal persist` you need to explicitly specify the data store when initializing `bal persist` in your application. as follows, + +``` +$ bal persist init --datastore redis +``` + +## Supported Ballerina Data Types +The following table lists the Ballerina data types supported by the Redis data store in `bal persist`. The specified data types will be converted to `string` during data insertion and then reverted to their original data types within Ballerina upon retrieval. + +| Ballerina Type | +|:----------------:| +| int | +| float | +| decimal | +| string | +| boolean | +| time:Date | +| time:TimeOfDay | +| time:Civil | +| time:Utc | +| enum | + +## Configuration + +You need to set values for the following basic configuration parameters in the `Config.toml` file in your project to use the Redis data store. + +| Parameter | Description | +|:----------:|:------------------------------------:| +| host | The hostname of the DB server. | +| port | The port of the DB server. | +| password | The password of the DB server. | + +The following is a sample `Config.toml` file with the Redis data store configuration. This is generated by the `bal persist generate` command. + +```toml +[.] +host = "localhost" +port = 6379 +password = "" +``` + +## How To Setup +Use docker as follows to create a DB server deployment. + +* Run `docker pull redis` to pull the official Redis Docker image from the Docker Hub +* Run `docker run --name -p 6379:6379 -d redis` diff --git a/ballerina/Package.md b/ballerina/Package.md new file mode 100644 index 0000000..4e6649c --- /dev/null +++ b/ballerina/Package.md @@ -0,0 +1,58 @@ +# Overview + +This module provide Redis database support for the `bal persist` feature, which provides functionality to store and query data from a Redis database through a data model instead of writing Redis commands. + +Since Redis is not the default datastore for `bal persist` you need to explicitly specify the data store when initializing `bal persist` in your application. as follows, + +``` +$ bal persist init --datastore redis +``` + +## Supported Ballerina Data Types +The following table lists the Ballerina data types supported by the Redis data store. Following data types will be converted to `string` when inserting data and converted back to relevent data types in ballerina when retrieving. + +| Ballerina Type | +|:----------------:| +| int | +| float | +| decimal | +| string | +| boolean | +| time:Date | +| time:TimeOfDay | +| time:Civil | +| time:Utc | +| enum | + +## Configuration + +You need to set values for the following basic configuration parameters in the `Config.toml` file in your project to use the Redis data store. + +| Parameter | Description | +|:----------:|:------------------------------------:| +| host | The hostname of the DB server. | +| port | The port of the DB server. | +| password | The password of the DB server. | + +The following is a sample `Config.toml` file with the Redis data store configuration. This is generated by the `bal persist generate` command. + +```toml +[.] +host = "localhost" +port = 6379 +password = "" +``` + +## How To Setup +Use docker as follows to create a DB server deployment. + +* Run `docker pull redis` to pull the official Redis Docker image from the Docker Hub +* Run `docker run --name -p 6379:6379 -d redis` + +## 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..5b86774 --- /dev/null +++ b/ballerina/build.gradle @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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}" + } +} + +description = 'Ballerina - Persist Ballerina Generator' + +def packageName = 'persist.redis' +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}", '') + } +} + +apply plugin: 'io.ballerina.plugin' + +ballerina { + packageOrganization = packageOrg + module = packageName + langVersion = ballerinaLangVersion + testCoverageParam = "--code-coverage --coverage-format=xml --includes=*" +} + +dependencies { + externalJars(group: 'io.ballerina.stdlib', name: 'persist-native', version: "${stdlibPersistVersion}") { + transitive = false + } +} + +task updateTomlFiles { + doLast { + def stdlibDependentPersistVersion = stripBallerinaExtensionVersion(project.stdlibPersistVersion) + + def newConfig = ballerinaTomlFilePlaceHolder.text.replace('@project.version@', project.version.toString()) + newConfig = newConfig.replace('@toml.version@', tomlVersion) + newConfig = newConfig.replace('@persist.version@', stdlibDependentPersistVersion) + newConfig = newConfig.replace('@persist.native.version@', project.stdlibPersistVersion) + ballerinaTomlFile.text = newConfig + } +} + +task commitTomlFiles { + doLast { + project.exec { + ignoreExitValue true + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'cmd', '/c', "git commit -m \"[Automated] Update native jar versions in toml files\" Ballerina.toml Dependencies.toml" + } else { + commandLine 'sh', '-c', "git commit -m \"[Automated] Update native jar versions in toml files\" Ballerina.toml Dependencies.toml" + } + } + } +} + +publishing { + publications { + maven(MavenPublication) { + artifact source: createArtifactZip, extension: 'zip' + } + } + repositories { + maven { + name = 'GitHubPackages' + url = uri("https://maven.pkg.github.com/ballerina-platform/module-${packageOrg}-${packageName}") + credentials { + username = System.getenv('publishUser') + password = System.getenv('publishPAT') + } + } + } +} + +static def checkExecResult(executionResult, failText, standardOutput) { + if (executionResult != null) { + Provider execResultProvider = executionResult.getProvider() + int exitCode = execResultProvider.get().getExitValue() + if (exitCode != 0) { + throw new GradleException('Non-zero exit value: ' + exitCode) + } + if (standardOutput.toString().contains(failText)) { + throw new GradleException('"' + failText + '" string in output: ' + standardOutput.toString()) + } + } else { + throw new GradleException('Returned a null execResult object') + } +} + +task createRedisTestDockerImage(type: Exec) { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + def standardOutput = new ByteArrayOutputStream() + commandLine 'sh', '-c', "docker build -f ${project.projectDir}/tests/resources/Dockerfile -t ballerina-persist-redis" + + " -q ${project.projectDir}/tests/resources" + doLast { + checkExecResult(executionResult, 'Error', standardOutput) + sleep(10 * 1000) + } + } else { + def standardOutput = new ByteArrayOutputStream() + commandLine 'cmd', '/c', "docker build -f ${project.projectDir}\\tests\\resources\\Dockerfile -t ballerina-persist-redis" + + " -q ${project.projectDir}\\tests\\resources\\" + doLast { + checkExecResult(executionResult, 'Error', standardOutput) + sleep(10 * 1000) + } + } +} + +def checkRedisTestDockerContainerStatus(containerName) { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + try { + return exec { + commandLine 'sh', '-c', "docker exec ${containerName} redis-cli" + }.exitValue + } catch (all) { + return 1; + } + } else { + try { + return exec { + commandLine 'cmd', '/c', "docker exec ${containerName} redis-cli" + }.exitValue + } catch (all) { + return 1; + } + } +} + +task startRedisTestDockerContainer(type: Exec) { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + def standardOutput = new ByteArrayOutputStream() + commandLine 'sh', '-c', + "docker run --rm -d --name ballerina-persist-redis -p 6379:6379 -d ballerina-persist-redis" + def healthCheck = 1; + def counter = 0; + doLast { + checkExecResult(executionResult, 'Error', standardOutput) + while (healthCheck != 0 && counter < 12) { + sleep(5 * 1000) + healthCheck = checkRedisTestDockerContainerStatus("ballerina-persist-redis") + counter = counter + 1; + } + if (healthCheck != 0) { + throw new GradleException("Docker container 'ballerina-persist-redis' health test exceeded timeout!") + } + } + } else { + def standardOutput = new ByteArrayOutputStream() + commandLine 'cmd', '/c', + "docker run --rm -d --name ballerina-persist-redis -p 6379:6379 -d ballerina-persist-redis" + def healthCheck = 1; + def counter = 0; + doLast { + checkExecResult(executionResult, 'Error', standardOutput) + while (healthCheck != 0 && counter < 12) { + sleep(5 * 1000) + healthCheck = checkRedisTestDockerContainerStatus("ballerina-persist-redis") + counter = counter + 1; + } + if (healthCheck != 0) { + throw new GradleException("Docker container 'ballerina-persist-redis' health test exceeded timeout!") + } + } + } +} + +task stopRedisTestDockerContainer() { + doLast { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + try { + def stdOut = new ByteArrayOutputStream() + exec { + commandLine 'sh', '-c', "docker stop ballerina-persist-redis" + standardOutput = stdOut + } + } catch (all) { + println("Process can safely ignore stopRedisTestDockerContainer task") + } + } else { + try { + def stdOut = new ByteArrayOutputStream() + exec { + commandLine 'cmd', '/c', "docker stop ballerina-persist-redis" + standardOutput = stdOut + } + } catch (all) { + println("Process can safely ignore stopRedisTestDockerContainer task") + } + } + } +} + +task pullRedisDependency(type: Exec) { + ignoreExitValue(true) + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + try { + String distributionBinPath = project.projectDir.absolutePath + "/build/jballerina-tools-${project.extensions.ballerina.langVersion}/bin" + commandLine 'sh', '-c', "$distributionBinPath/bal pull ballerinax/redis:${stdlibRedisVersion}" + } catch (all) { + return 1 + } + } else { + try { + String distributionBinPath = project.projectDir.absolutePath + "/build/jballerina-tools-${project.extensions.ballerina.langVersion}/bin" + commandLine 'cmd', '/c', "$distributionBinPath/bal.bat pull ballerinax/redis:${stdlibRedisVersion}" + } catch (all) { + return 1 + } + } +} + +updateTomlFiles.dependsOn copyStdlibs +pullRedisDependency.dependsOn unpackJballerinaTools +startRedisTestDockerContainer.dependsOn createRedisTestDockerImage + +build.dependsOn "generatePomFileForMavenPublication" +build.dependsOn ":${packageName}-native:build" +build.dependsOn pullRedisDependency +build.finalizedBy stopRedisTestDockerContainer + +test.dependsOn ":${packageName}-native:build" +test.dependsOn startRedisTestDockerContainer diff --git a/ballerina/constants.bal b/ballerina/constants.bal new file mode 100644 index 0000000..aba8c00 --- /dev/null +++ b/ballerina/constants.bal @@ -0,0 +1,19 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + +public const string KEY_SEPERATOR = ":"; +public const string MANY_ASSOCIATION_SEPERATOR = "[]."; +public const string ASSOCIATION_SEPERATOR = "."; diff --git a/ballerina/errors.bal b/ballerina/errors.bal new file mode 100644 index 0000000..0f2eb3e --- /dev/null +++ b/ballerina/errors.bal @@ -0,0 +1,39 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; + +# Generates a new `persist:ConstraintViolationError` with the given parameters. +# +# + entity - The name of the entity +# + refEntity - The entity is being reffered +# + return - The generated `persist:ConstraintViolationError` +public isolated function getConstraintViolationError(string entity, string refEntity) +returns persist:ConstraintViolationError { + string message = string `An association constraint failed between entities '${entity}' and '${refEntity}'`; + return error persist:ConstraintViolationError(message); +} + + +# Generates a new `persist:AlreadyExistsError` with the given parameters. +# +# + entity - The name of the entity +# + keyCount - The number of keys already exists +# + return - The generated `persist:AlreadyExistsError` +public isolated function getAlreadyExistsError(string entity, int keyCount) returns persist:AlreadyExistsError { + return error persist:AlreadyExistsError( + string `Record(s) already exist with the same key for the entity '${entity}'. Number of keys exists : ${keyCount}`); +} diff --git a/ballerina/field_types.bal b/ballerina/field_types.bal new file mode 100644 index 0000000..0f5c10a --- /dev/null +++ b/ballerina/field_types.bal @@ -0,0 +1,32 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/time; + +# Generic type that can used to store any of the types supported by Redis +# +public type RedisFieldType string|int|decimal|boolean|float|time:Date|time:TimeOfDay|time:Civil|time:Utc; + +# Generic type that can used to store any of time types supported by Redis +# +public type RedisTimeType time:Date|time:TimeOfDay|time:Civil|time:Utc; + +# Generic type that can used to store any of basic numeric types supported by Redis +# +public type RedisNumericType int|decimal|boolean|float; + +# Generic type that can used to store any of basic types supported by Redis +# +public type RedisBasicType int|string|decimal|boolean|float; diff --git a/ballerina/init.bal b/ballerina/init.bal new file mode 100644 index 0000000..02a4e8c --- /dev/null +++ b/ballerina/init.bal @@ -0,0 +1,24 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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 init() { + setModule(); +} + +isolated function setModule() = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.ModuleUtils" +} external; diff --git a/ballerina/metadata_types.bal b/ballerina/metadata_types.bal new file mode 100644 index 0000000..4abd5d5 --- /dev/null +++ b/ballerina/metadata_types.bal @@ -0,0 +1,137 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + +# Represents the metadata of an entity. +# +# + entityName - Name of the entity +# + collectionName - Collection name of the entity +# + fieldMetadata - Metadata of all the fields of the entity +# + keyFields - Names of the identity fields +# + refMetadata - Metadata of the fields that is being reffered +public type RedisMetadata record {| + string entityName; + string collectionName; + map fieldMetadata; + string[] keyFields; + map refMetadata?; +|}; + +# Represents the metadata associated with a field from a related entity. +# +# + relation - The relational metadata associated with the field +public type EntityFieldMetadata record {| + RelationMetadata relation; +|}; + +# Represents the metadata associated with a simple field in the entity record. +# +# + fieldName - The name of the Redis document field to which the object field is mapped +# + fieldDataType - The data type of the object field to which the Redis document field mapped +public type SimpleFieldMetadata record {| + string fieldName; + DataType fieldDataType; +|}; + +# Represents the metadata associated with a field of an entity. +# Only used by the generated persist clients and `persist:RedisClient`. +# +public type FieldMetadata SimpleFieldMetadata|EntityFieldMetadata; + +# Represents the metadata associated with a relation. +# Only used by the generated persist clients and `persist:RedisClient`. +# +# + entityName - The name of the entity represented in the relation +# + refField - The name of the refered field in the Redis document +# + refFieldDataType - The data type of the object field to which the refered field in +# Redis document is mapped +public type RelationMetadata record {| + string entityName; + string refField; + DataType refFieldDataType; +|}; + +# Represents the metadata associated with relations +# Only used by the generated persist clients and `persist:RedisClient`. +# +# + entity - The name of the entity that is being joined +# + fieldName - The name of the field in the `entity` that is being joined +# + refCollection - The name of the Redis collection to be joined +# + refFields - The names of the fields of the refered collection +# + joinFields - The names of the join fields +# + joinCollection - The name of the joining collection used for a many-to-many relation +# + joiningRefFields - The names of the refered fields in the joining collection +# + joiningJoinFields - The names of the join fields in the joining collection +# + 'type - The type of the relation +public type RefMetadata record {| + typedesc entity; + string fieldName; + string refCollection; + string[] refFields; + string[] joinFields; + string joinCollection?; + string[] joiningRefFields?; + string[] joiningJoinFields?; + CardinalityType 'type; +|}; + +# Represents the cardinality of the relationship +# Only used by the generated persist clients and `persist:RedisClient`. +# +# + ONE_TO_ONE - The association type is a one-to-one association +# + ONE_TO_MANY - The entity is in the 'one' side of a one-to-many association +# + MANY_TO_ONE - The entity is in the 'many' side of a one-to-many association +public enum CardinalityType { + ONE_TO_ONE, + ONE_TO_MANY, + MANY_TO_ONE +} + +# Represents the type of the field data. +# Only used by the generated persist clients and `persist:RedisClient`. +# +public enum DataType { + INT, + STRING, + FLOAT, + DECIMAL, + BOOLEAN, + DATE, + TIME_OF_DAY, + CIVIL, + UTC, + ENUM +} + +# Represents the type of the redis supported data structures. +# Only used by the `persist:RedisClient`. +# +# + REDIS_STRING - `string` type +# + REDIS_SET - `set` type +# + REDIS_HASH - `hash` type +enum RedisDataType { + REDIS_STRING = "string", + REDIS_SET = "set", + REDIS_HASH = "hash" +} + +# Represents the types of Metadata in a `persist:RedisClient`. +# Only used by the `persist:RedisClient`. +# +enum MetaData { + FIELD_DATA_TYPE = "fieldDataType", + RELATION = "relation", + REF_FIELD_DATA_TYPE = "refFieldDataType" +} diff --git a/ballerina/redis_client.bal b/ballerina/redis_client.bal new file mode 100644 index 0000000..f6f6f57 --- /dev/null +++ b/ballerina/redis_client.bal @@ -0,0 +1,951 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/lang.regexp; +import ballerina/log; +import ballerina/persist; +import ballerina/time; +import ballerinax/redis; + +# The client used by the generated persist clients to abstract and +# execute Redis database operations that are required to perform CRUD operations. +public isolated client class RedisClient { + + private final redis:Client dbClient; + + private final string entityName; + private final string collectionName; + private final map & readonly fieldMetadata; + private final string[] & readonly keyFields; + private final map & readonly refMetadata; + + # Initializes the `RedisClient`. + # + # + dbClient - The `redis:Client`, which is used to execute Redis database operations. + # + metadata - Metadata of the entity + # + return - A `persist:Error` if the client creation fails + public isolated function init(redis:Client dbClient, RedisMetadata & readonly metadata) returns persist:Error? { + self.entityName = metadata.entityName; + self.collectionName = metadata.collectionName; + self.fieldMetadata = metadata.fieldMetadata; + self.keyFields = metadata.keyFields; + self.dbClient = dbClient; + (map & readonly)? refMetadata = metadata.refMetadata; + if refMetadata is map & readonly { + self.refMetadata = refMetadata; + } else { + self.refMetadata = {}; + } + } + + # Performs a batch `HGET` operation to get entity instances as a stream + # + # + rowType - The type description of the entity to be retrieved + # + typeMap - The data type map of the target type + # + key - Key for the record + # + fields - The fields to be retrieved + # + include - The associations to be retrieved + # + typeDescriptions - The type descriptions of the relations to be retrieved + # + return - An `record {|anydata...;|}` containing the requested record + # or a `persist:Error` if the operation fails + public isolated function runReadByKeyQuery(typedesc rowType, map typeMap, anydata key, + string[] fields = [], string[] include = [], typedesc[] typeDescriptions = []) + returns record {|anydata...;|}|persist:Error { + // Generate the key + string recordKey = string `${self.collectionName}${self.getKey(key)}`; + do { + // Handling simple fields + record {} 'object = check self.querySimpleFieldsByKey(typeMap, recordKey, fields); + // Handling relation fields + check self.getManyRelations(typeMap, 'object, fields, include); + return check 'object.cloneWithType(rowType); + } on fail error e { + if e is persist:NotFoundError || e is persist:Error { + return e; + } + return error persist:Error(e.message(), e); + } + } + + # Performs a batch `HGET` operation to get entity instances as a stream + # + # + rowType - The type description of the entity to be retrieved + # + typeMap - The data types of the record + # + fields - The fields to be retrieved + # + include - The associations to be retrieved + # + return - A stream of `stream` containing the requested records + # or a `persist:Error` if the operation fails + public isolated function runReadQuery(typedesc rowType, map typeMap, string[] fields = [], + string[] include = []) returns stream|persist:Error { + // Get all the keys + string pattern = string `${self.collectionName}${KEY_SEPERATOR}*`; + string[]|error keys = self.dbClient->keys(pattern); + if keys is error { + return error persist:Error(keys.message(), keys); + } + self.logQuery(KEYS, pattern); + + // Get records one by one using the key + record {}[] result = []; + foreach string key in keys { + // Validate the type + string redisType = check self.dbClient->redisType(key); + self.logQuery(REDISTYPE, key); + if redisType != REDIS_HASH { + continue; + } + + // Handling simple fields + record {} 'object = check self.querySimpleFieldsByKey(typeMap, key, fields); + result.push('object); + } on fail error e { + if e is persist:NotFoundError || e is persist:Error { + return e; + } + return error persist:Error(e.message(), e); + } + return stream from record {} rec in result + select rec; + } + + # Performs a batch `HMSET` operation to insert entity instances into a collection. + # + # + insertRecords - The entity records to be inserted into the collection + # + return - A `string` containing the information of the database operation execution + # or a `persist:Error` if the operation fails + public isolated function runBatchInsertQuery(record {}[] insertRecords) returns string|persist:Error { + + string|error result; + + // Verify key existance + string[] insertKeys = []; + foreach var insertRecord in insertRecords { + // Generate the key + string key = self.collectionName; + foreach string keyField in self.keyFields { + key += string `${KEY_SEPERATOR}${insertRecord[keyField].toString()}`; + } + insertKeys.push(key); + } + int|error isKeyExists = self.dbClient->exists(insertKeys); + if isKeyExists is error { + return error persist:Error(isKeyExists.message(), isKeyExists); + } + self.logQuery(EXISTS, insertKeys); + if isKeyExists != 0 { + return getAlreadyExistsError(self.collectionName, isKeyExists); + } + + // For each record, do HMSET + foreach var insertRecord in insertRecords { + + // Check time related data types + boolean isContainTimeType = false; + foreach string recordfield in insertRecord.keys() { + (FieldMetadata & readonly)? fieldMetaDataValue = self.fieldMetadata[recordfield]; + if fieldMetaDataValue is SimpleFieldMetadata { + DataType dataType = fieldMetaDataValue.fieldDataType; + if dataType == DATE || dataType == TIME_OF_DAY || dataType == UTC || dataType == CIVIL { + isContainTimeType = true; + continue; + } + } + } + + // Generate the key + string keySuffix = ""; + foreach string keyField in self.keyFields { + keySuffix += string `${KEY_SEPERATOR}${insertRecord[keyField].toString()}`; + } + + // Check for any relation field constraints + persist:Error? checkConstraints = self.checkRelationFieldConstraints(keySuffix, insertRecord); + if checkConstraints is persist:ConstraintViolationError { + return checkConstraints; + } + string key = string `${self.collectionName}${keySuffix}`; + + // Insert the record + if isContainTimeType { + result = self.dbClient->hMSet(key, check self.newRecordWithDateTime(insertRecord)); + } else { + result = self.dbClient->hMSet(key, insertRecord); + } + self.logQuery(HMSET, {key: key, "record": insertRecord.toJsonString()}); + } on fail persist:Error e { + return e; + } + + if result is string { + return result; + } + return error persist:Error(result.message(), result); + } + + # Performs redis `DEL` operation to delete an entity record from the database. + # + # + key - The ordered keys used to delete an entity record + # + return - `()` if the operation is performed successfully + # or a `persist:Error` if the operation fails + public isolated function runDeleteQuery(anydata key) returns persist:Error? { + string keySuffix = string `${self.getKey(key)}`; + string keyWithPrefix = string `${self.collectionName}${keySuffix}`; + // Delete the record + do { + // Check for references + string[] allRefFields = []; + foreach RefMetadata refMedaData in self.refMetadata { + allRefFields.push(...refMedaData.joinFields); + } + + // Remove any references if exists + if allRefFields.length() > 0 { + map currentObject = check self.dbClient->hMGet(keyWithPrefix, allRefFields); + self.logQuery(HMGET, {key: keyWithPrefix, fieldNames: allRefFields.toJsonString()}); + foreach RefMetadata refMedaData in self.refMetadata { + string refKey = refMedaData.refCollection; + foreach string refField in refMedaData.joinFields { + refKey += string `${KEY_SEPERATOR}${currentObject[refField].toString()}`; + } + string setKey = string `${refKey}${KEY_SEPERATOR}${self.collectionName}`; + _ = check self.dbClient->sRem(setKey, [keySuffix.substring(1)]); + self.logQuery(SREM, {key: setKey, suffixes: [keySuffix].toJsonString()}); + } + } + + // Remove the record + _ = check self.dbClient->del([keyWithPrefix]); + self.logQuery(DEL, [keyWithPrefix]); + } on fail error e { + return error persist:Error(e.message(), e); + } + } + + # Performs redis `HSET` operation to update an entity record from the database. + # + # + key - The ordered keys used to update an entity record + # + updateRecord - The new record to be updated + # + return - An Error if the new record is missing a keyfield + public isolated function runUpdateQuery(anydata key, record {} updateRecord) returns persist:Error? { + // Generate the key + string recordKey = string `${self.collectionName}${self.getKey(key)}`; + string recordKeySuffix = self.getKey(key); + + // Verify the existence of the key + do { + int isKeyExists = check self.dbClient->exists([recordKey]); + self.logQuery(EXISTS, [recordKey]); + if isKeyExists == 0 { + return persist:getNotFoundError(self.collectionName, recordKey); + } + } on fail error e { + if e is persist:NotFoundError { + return e; + } + return error persist:Error(e.message(), e); + } + + // Check time related data types + boolean isContainTimeType = false; + foreach string recordfield in updateRecord.keys() { + (FieldMetadata & readonly)? fieldMetaDataValue = self.fieldMetadata[recordfield]; + if fieldMetaDataValue is SimpleFieldMetadata { + DataType dataType = fieldMetaDataValue.fieldDataType; + if dataType == DATE || dataType == TIME_OF_DAY || dataType == UTC || dataType == CIVIL { + isContainTimeType = true; + continue; + } + } + } + + record {} newUpdateRecord = {}; + if isContainTimeType { + newUpdateRecord = check self.newRecordWithDateTime(updateRecord); + } else { + newUpdateRecord = updateRecord; + } + + // Get the original record before update + map<()> updatedEntities = {}; + map|error prevRecord = self.dbClient->hMGet(recordKey, newUpdateRecord.keys()); + if prevRecord is error { + return error persist:Error(prevRecord.message(), prevRecord); + } + self.logQuery(HMGET, {key: recordKey, fieldNames: newUpdateRecord.keys().toJsonString()}); + + // Check the validity of new associations + foreach RefMetadata refMetaData in self.refMetadata { + string[] joinFields = refMetaData.joinFields; + + // Recreate the key + string relatedRecordKey = refMetaData.refCollection; + foreach string joinField in joinFields { + if newUpdateRecord.hasKey(joinField) { + updatedEntities[refMetaData.refCollection] = (); + relatedRecordKey += string `${KEY_SEPERATOR}${newUpdateRecord[joinField].toString()}`; + } else { + relatedRecordKey += string `${KEY_SEPERATOR}${prevRecord[joinField].toString()}`; + } + } + + // Verify the new associated entities does exists + if updatedEntities.hasKey(refMetaData.refCollection) { + int isKeyExists = check self.dbClient->exists([relatedRecordKey]); + self.logQuery(EXISTS, [relatedRecordKey]); + if isKeyExists != 0 { + // Verify the key type as a HASH + string redisType = check self.dbClient->redisType(relatedRecordKey); + self.logQuery(REDISTYPE, relatedRecordKey); + if redisType == REDIS_HASH { + continue; + } + } + // Return a constrain violation error if new associations does not exists + return getConstraintViolationError(self.collectionName, refMetaData.refCollection); + } + } on fail error e { + if e is persist:ConstraintViolationError { + return e; + } + return error persist:Error(e.message(), e); + } + + // Verify the availablity of new associations + // Eg: Reffered record might already in a ONE-TO-ONE relationship + foreach RefMetadata refMetaData in self.refMetadata { + if !updatedEntities.hasKey(refMetaData.refCollection) { + continue; + } + string[] joinFields = refMetaData.joinFields; + string newRelatedRecordKey = refMetaData.refCollection; + foreach string joinField in joinFields { + if newUpdateRecord.hasKey(joinField) { + newRelatedRecordKey += string `${KEY_SEPERATOR}${newUpdateRecord[joinField].toString()}`; + } else { + newRelatedRecordKey += string `${KEY_SEPERATOR}${prevRecord[joinField].toString()}`; + } + } + + // Get keys of existing associations + string setKey = string `${newRelatedRecordKey}${KEY_SEPERATOR}${self.collectionName}`; + int isKeyExists = check self.dbClient->exists([setKey]); + self.logQuery(EXISTS, [setKey]); + if isKeyExists != 0 { + // Verify the key type as a SET + string redisType = check self.dbClient->redisType(setKey); + self.logQuery(REDISTYPE, setKey); + if redisType == REDIS_SET { + // Check existing associations for ONE-TO-ONE + if refMetaData.'type == ONE_TO_ONE { + int cardinality = check self.dbClient->sCard(setKey); + self.logQuery(SCARD, setKey); + if cardinality > 0 { + return getConstraintViolationError(self.collectionName, refMetaData.refCollection); + } + } + } + } + } on fail error e { + if e is persist:ConstraintViolationError { + return e; + } + return error persist:Error(e.message(), e); + } + + // Update + foreach string updatedField in newUpdateRecord.keys() { + _ = check self.dbClient->hSet(recordKey, updatedField, + newUpdateRecord[updatedField].toString()); + self.logQuery(HSET, { + key: recordKey, + fieldName: updatedField, + newValue: newUpdateRecord[updatedField].toJsonString() + }); + } on fail error e { + return error persist:Error(e.message(), e); + } + + // Add new association to the SET + foreach RefMetadata refMetaData in self.refMetadata { + if !updatedEntities.hasKey(refMetaData.refCollection) { + continue; + } + string[] joinFields = refMetaData.joinFields; + string newRelatedRecordKey = refMetaData.refCollection; + string prevRelatedRecordKey = refMetaData.refCollection; + foreach string joinField in joinFields { + if newUpdateRecord.hasKey(joinField) { + newRelatedRecordKey += string `${KEY_SEPERATOR}${newUpdateRecord[joinField].toString()}`; + } else { + newRelatedRecordKey += string `${KEY_SEPERATOR}${prevRecord[joinField].toString()}`; + } + prevRelatedRecordKey += string `${KEY_SEPERATOR}${prevRecord[joinField].toString()}`; + } + // Attach to new association + string newSetKey = string `${newRelatedRecordKey}${KEY_SEPERATOR}${self.collectionName}`; + int|error sAdd = self.dbClient->sAdd(newSetKey, [recordKeySuffix.substring(1)]); + if sAdd is error { + return error persist:Error(sAdd.message(), sAdd); + } + self.logQuery(SADD, {key: newSetKey, suffixes: [recordKeySuffix].toJsonString()}); + + // Detach from previous association + string prevSetKey = string `${prevRelatedRecordKey}${KEY_SEPERATOR}${self.collectionName}`; + int|error sRem = self.dbClient->sRem(prevSetKey, [recordKeySuffix.substring(1)]); + if sRem is error { + return error persist:Error(sRem.message(), sRem); + } + self.logQuery(SREM, {key: prevSetKey, suffixes: [recordKeySuffix].toJsonString()}); + } + } + + # Retrieves all the associations of a given object + # + # + typeMap - The data types of the record + # + object - The object of the interest + # + fields - The fields to be retrieved + # + include - The associations to be retrieved + # + return - A `persist:Error` if the operation fails + public isolated function getManyRelations(map typeMap, record {} 'object, string[] fields, + string[] include) returns persist:Error? { + foreach int i in 0 ..< include.length() { + string entity = include[i]; + (RefMetadata & readonly)? refMetaData = self.refMetadata[entity]; + if refMetaData == () { + continue; + } + + // Check for one to many relationships + CardinalityType cardinalityType = ONE_TO_MANY; + string[] relationFields = from string 'field in fields + where 'field.startsWith(string `${entity}${MANY_ASSOCIATION_SEPERATOR}`) + select 'field.substring(entity.length() + 3, 'field.length()); + + // Check for one to one relationships + if relationFields.length() == 0 { + relationFields = from string 'field in fields + where 'field.startsWith(string `${entity}${ASSOCIATION_SEPERATOR}`) + select 'field.substring(entity.length() + 1, 'field.length()); + + if relationFields.length() != 0 { + cardinalityType = ONE_TO_ONE; + } + } + + if relationFields.length() == 0 { + continue; + } + + // Get key suffixes of asslociated records + string[]|error keySuffixes = self.getRelatedEntityKeySuffixes(entity, 'object); + if keySuffixes is error || keySuffixes.length() == 0 { + if cardinalityType == ONE_TO_MANY { + 'object[entity] = []; + } else { + 'object[entity] = {}; + } + continue; + } + + // Get data one by one using the key + record {}[] associatedRecords = []; + foreach string key in keySuffixes { + // Handling simple fields of the associated record + record {} valueToRecord = check self.queryRelationFieldsByKey(entity, cardinalityType, + string `${refMetaData.refCollection}${key}`, relationFields); + + foreach string refField in valueToRecord.keys() { + if relationFields.indexOf(refField) is () { + _ = valueToRecord.remove(refField); + } + } + associatedRecords.push(valueToRecord); + } + + if associatedRecords.length() > 0 { + if cardinalityType == ONE_TO_ONE { + 'object[entity] = associatedRecords[0]; + } else { + 'object[entity] = associatedRecords; + } + } + } on fail persist:Error e { + return e; + } + + self.removeUnwantedFields('object, fields); + self.removeNonExistOptionalFields('object); + } + + public isolated function getKeyFields() returns string[] { + return self.keyFields; + } + + // Private helper methods + private isolated function getKey(anydata key) returns string { + string keyValue = ""; + if key is map { + foreach string compositeKey in key.keys() { + keyValue += string `${KEY_SEPERATOR}${key[compositeKey].toString()}`; + } + return keyValue; + } + return string `${KEY_SEPERATOR}${key.toString()}`; + } + + private isolated function getKeyFromObject(record {} 'object) returns string { + string key = self.collectionName; + foreach string keyField in self.keyFields { + key += string `${KEY_SEPERATOR}${'object[keyField].toString()}`; + } + return key; + } + + private isolated function getRelatedEntityKeySuffixes(string entity, record {} 'object) returns string[]|error { + (RefMetadata & readonly)? refMetaData = self.refMetadata[entity]; + if refMetaData == () { + return []; + } + + if refMetaData.joinFields == self.keyFields { + // Non-owner have direct access to the association set + string setKey = string `${self.getKeyFromObject('object)}${KEY_SEPERATOR}${refMetaData.refCollection}`; + string[] keys = check self.dbClient->sMembers(setKey); + self.logQuery(SMEMBERS, setKey); + return from string key in keys + select KEY_SEPERATOR + key; + } else { + map recordWithRefFields = check self.dbClient->hMGet(self.getKeyFromObject('object), + refMetaData.joinFields); + self.logQuery(HMGET, { + key: self.getKeyFromObject('object), + "record": refMetaData.joinFields.toJsonString() + }); + string key = ""; + foreach string joinField in refMetaData.joinFields { + key += string `${KEY_SEPERATOR}${recordWithRefFields[joinField].toString()}`; + } + return [key]; + } + } + + private isolated function querySimpleFieldsByKey(map typeMap, string key, string[] fields) + returns record {|anydata...;|}|persist:Error { + string[] simpleFields = self.getTargetSimpleFields(fields, typeMap); + + do { + // Retrieve the record + map value = check self.dbClient->hMGet(key, simpleFields); + self.logQuery(HMGET, {key: key, fieldNames: simpleFields.toJsonString()}); + if self.isNoRecordFound(value) { + return persist:getNotFoundError(self.entityName, key); + } + record {} valueToRecord = {}; + foreach string fieldKey in value.keys() { + // Convert the data type from 'any' to relevent type + valueToRecord[fieldKey] = check self.dataConverter( + self.fieldMetadata[fieldKey], value[fieldKey]); + } + return valueToRecord; + } on fail error e { + if e is persist:NotFoundError { + return e; + } + return error persist:Error(e.message(), e); + } + } + + private isolated function queryRelationFieldsByKey(string entity, CardinalityType cardinalityType, string key, + string[] fields) returns record {|anydata...;|}|persist:Error { + string[] relationFields = fields.clone(); + (RefMetadata & readonly)? refMetaData = self.refMetadata[entity]; + if refMetaData == () { + return error persist:Error(string `Undefined relation between ${self.entityName} and ${entity}`); + } + + // Add required missing reference fields + foreach string refKeyField in refMetaData.refFields { + if relationFields.indexOf(refKeyField) is () { + relationFields.push(refKeyField); + } + } + + do { + // Retrieve related records + map value = check self.dbClient->hMGet(key, relationFields); + self.logQuery(HMGET, {key: key, fieldNames: relationFields.toJsonString()}); + if self.isNoRecordFound(value) { + return error persist:Error(string `No '${entity}' found for the given key '${key}'`); + } + + record {} valueToRecord = {}; + string fieldMetadataKeyPrefix = entity; + if cardinalityType == ONE_TO_MANY { + fieldMetadataKeyPrefix += MANY_ASSOCIATION_SEPERATOR; + } else { + fieldMetadataKeyPrefix += ASSOCIATION_SEPERATOR; + } + + foreach string fieldKey in value.keys() { + // convert the data type from 'any' to relevant type + valueToRecord[fieldKey] = check self.dataConverter( + self.fieldMetadata[string `${fieldMetadataKeyPrefix}${fieldKey}`], + value[fieldKey]); + } + return valueToRecord; + } on fail error e { + if e is persist:NotFoundError || e is persist:Error { + return e; + } + return error persist:Error(e.message(), e); + } + } + + private isolated function getTargetSimpleFields(string[] fields, map typeMap) returns string[] { + string[] requiredFields = from string 'field in fields + where !'field.includes(".") && typeMap.hasKey('field) + select 'field; + foreach string keyField in self.keyFields { + if requiredFields.indexOf(keyField) == () { + requiredFields.push(keyField); + } + } + return requiredFields; + } + + private isolated function removeNonExistOptionalFields(record {} 'object) { + foreach string key in 'object.keys() { + if 'object[key] == () { + _ = 'object.remove(key); + } + } + } + + private isolated function isNoRecordFound(map value) returns boolean { + foreach string key in value.keys() { + if value[key] != () { + return false; + } + } + return true; + } + + private isolated function removeUnwantedFields(record {} 'object, string[] fields) { + string[] keyFields = self.keyFields; + foreach string keyField in keyFields { + if fields.indexOf(keyField) is () && 'object.hasKey(keyField) { + _ = 'object.remove(keyField); + } + } + } + + private isolated function checkRelationFieldConstraints(string key, record {} insertRecord) returns persist:Error? { + if self.refMetadata != {} { + foreach RefMetadata & readonly refMetadataValue in self.refMetadata { + // If the entity is not the relation owner + if refMetadataValue.joinFields == self.keyFields { + continue; + } + + // Generate the key to reference record + string refRecordKey = refMetadataValue.refCollection; + foreach string joinField in refMetadataValue.joinFields { + refRecordKey += string `${KEY_SEPERATOR}${insertRecord[joinField].toString()}`; + } + + // Check the cardinality of refered entity record + string setKey = string `${refRecordKey}${KEY_SEPERATOR}${self.collectionName}`; + int|error sCard = self.dbClient->sCard(setKey); + self.logQuery(SCARD, setKey); + if sCard is int && sCard > 0 && refMetadataValue.'type == ONE_TO_ONE { + // If the refered record is already in an association + return getConstraintViolationError(self.collectionName, refMetadataValue.refCollection); + } + + // Associate current record with the refered record + int|error sAdd = self.dbClient->sAdd(setKey, [key.substring(1)]); + self.logQuery(SADD, {key: setKey, suffixes: [key].toJsonString()}); + if sAdd is error { + return error persist:Error(sAdd.message(), sAdd); + } + + map value = check self.dbClient->hMGet(refRecordKey, refMetadataValue.refFields); + self.logQuery(HMGET, {key: refRecordKey, fieldNames: refMetadataValue.refFields.toJsonString()}); + if self.isNoRecordFound(value) { + return getConstraintViolationError(self.collectionName, refMetadataValue.refCollection); + } + } on fail error e { + if e is persist:ConstraintViolationError { + return e; + } + return error persist:Error(e.message(), e); + } + } + } + + private isolated function newRecordWithDateTime(record {} insertRecord) returns record {}|persist:Error { + record {} newRecord = {}; + foreach string recordfield in insertRecord.keys() { + (FieldMetadata & readonly)? fieldMetaDataValue = self.fieldMetadata[recordfield]; + if fieldMetaDataValue is SimpleFieldMetadata { + DataType dataType = fieldMetaDataValue.fieldDataType; + + if dataType == DATE || dataType == TIME_OF_DAY || dataType == UTC || dataType == CIVIL { + RedisTimeType?|error timeValue = insertRecord.get(recordfield).ensureType(); + if timeValue is error { + return error persist:Error(timeValue.message(), timeValue); + } + + string|persist:Error? timeValueInString = self.timeToString(timeValue); + if timeValueInString is persist:Error { + return timeValueInString; + } + + newRecord[recordfield] = timeValueInString; + } else { + newRecord[recordfield] = insertRecord[recordfield]; + } + } + } + return newRecord; + } + + private isolated function timeToString(RedisTimeType? timeValue) returns string|persist:Error? { + if timeValue is () { + return (); + } + + if timeValue is time:Civil { + string|error civilToStringResult = self.civilToString(timeValue); + if civilToStringResult is error { + return error persist:Error(civilToStringResult.message(), civilToStringResult); + } + return civilToStringResult; + } + + if timeValue is time:Utc { + return time:utcToString(timeValue); + } + + if timeValue is time:Date { + return string `${timeValue.day}-${timeValue.month}-${timeValue.year}`; + } + + if timeValue is time:TimeOfDay { + return string `${timeValue.hour}:${timeValue.minute}:${(timeValue.second).toString()}`; + } + + return error persist:Error("Error: unsupported time format"); + } + + private isolated function stringToTime(string timeValue, DataType dataType) returns RedisTimeType|error { + if dataType == TIME_OF_DAY { + string[] timeValues = re `:`.split(timeValue); + time:TimeOfDay output = { + hour: check int:fromString(timeValues[0]), + minute: check int:fromString( + timeValues[1]), + second: check decimal:fromString(timeValues[2]) + }; + return output; + } else if dataType == DATE { + string[] timeValues = re `-`.split(timeValue); + time:Date output = { + day: check int:fromString(timeValues[0]), + month: check int:fromString(timeValues[1]), + year: check int:fromString(timeValues[2]) + }; + return output; + } else if dataType == CIVIL { + return self.civilFromString(timeValue); + } else if dataType == UTC { + return time:utcFromString(timeValue); + } else { + return error persist:Error("Error: unsupported time format"); + } + } + + private isolated function civilToString(time:Civil civil) returns string|error { + string civilString = string `${civil.year}-${(civil.month.abs() > 9 ? civil.month + : string `0${civil.month}`)}-${(civil.day.abs() > 9 ? civil.day : string `0${civil.day}`)}`; + civilString += string `T${(civil.hour.abs() > 9 ? civil.hour + : string `0${civil.hour}`)}:${(civil.minute.abs() > 9 ? civil.minute : string `0${civil.minute}`)}`; + if civil.second !is () { + time:Seconds seconds = civil.second; + civilString += string `:${(seconds.abs() > (check decimal:fromString("9")) ? seconds + : string `0${seconds}`)}`; + } + if civil.utcOffset !is () { + time:ZoneOffset zoneOffset = civil.utcOffset; + civilString += (zoneOffset.hours >= 0 ? "+" : "-"); + civilString += string `${zoneOffset.hours.abs() > 9 ? zoneOffset.hours.abs() + : string `0${zoneOffset.hours.abs()}`}`; + civilString += string `:${(zoneOffset.minutes.abs() > 9 ? zoneOffset.minutes.abs() + : string `0${zoneOffset.minutes.abs()}`)}`; + time:Seconds? seconds = zoneOffset.seconds; + if seconds !is () { + civilString += string `:${(seconds.abs() > 9d ? seconds : string `0${seconds.abs()}`)}`; + } else { + civilString += string `:00`; + } + } + if civil.timeAbbrev !is () { + civilString += string `(${civil.timeAbbrev})`; + } + return civilString; + } + + private isolated function civilFromString(string civilString) returns time:Civil|error { + time:ZoneOffset? zoneOffset = (); + string civilTimeString = ""; + string civilDateString = ""; + string? timeAbbrev = (); + regexp:Span? find = re `\(.*\)`.find(civilString.trim(), 0); + if find !is () { + timeAbbrev = civilString.trim().substring(find.startIndex + 1, find.endIndex - 1); + } + string[] civilArray = re `T`.split(re `\(.*\)`.replace(civilString.trim(), "")); + civilDateString = civilArray[0]; + find = re `\+|-`.find(civilArray[1], 0); + if find !is () { + int sign = +1; + if civilArray[1].includes("-") { + sign = -1; + } + string[] civilTimeOffsetArray = re `\+|-`.split(civilArray[1]); + civilTimeString = civilTimeOffsetArray[0]; + string[] zoneOffsetStringArray = re `:`.split(civilTimeOffsetArray[1]); + zoneOffset = { + hours: sign * (check int:fromString(zoneOffsetStringArray[0])), + minutes: sign * (check int:fromString(zoneOffsetStringArray[1])), + seconds: sign * (check decimal:fromString(zoneOffsetStringArray[2])) + }; + } else { + civilTimeString = civilArray[1]; + } + string[] civilTimeStringArray = re `:`.split(civilTimeString); + string[] civilDateStringArray = re `-`.split(civilDateString); + int year = check int:fromString(civilDateStringArray[0]); + int month = check int:fromString(civilDateStringArray[1]); + int day = check int:fromString(civilDateStringArray[2]); + int hour = check int:fromString(civilTimeStringArray[0]); + int minute = check int:fromString(civilTimeStringArray[1]); + decimal second = check decimal:fromString(civilTimeStringArray[2]); + return { + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + timeAbbrev: timeAbbrev, + utcOffset: zoneOffset + }; + } + + private isolated function dataConverter(FieldMetadata & readonly fieldMetaData, anydata value) + returns ()|boolean|string|float|decimal|int|RedisTimeType|error { + + // Return nil if value is nil + if value is () { + return (); + } + + if (fieldMetaData is SimpleFieldMetadata && fieldMetaData[FIELD_DATA_TYPE] == INT) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == INT) { + return check int:fromString(value.toString()); + + } else if (fieldMetaData is SimpleFieldMetadata && (fieldMetaData[FIELD_DATA_TYPE] == STRING)) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == STRING) { + return value; + + } else if (fieldMetaData is SimpleFieldMetadata && fieldMetaData[FIELD_DATA_TYPE] == FLOAT) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == FLOAT) { + return check float:fromString(value); + + } else if (fieldMetaData is SimpleFieldMetadata && fieldMetaData[FIELD_DATA_TYPE] == DECIMAL) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == DECIMAL) { + return check decimal:fromString(value); + + } else if (fieldMetaData is SimpleFieldMetadata && fieldMetaData[FIELD_DATA_TYPE] == BOOLEAN) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == BOOLEAN) { + return check boolean:fromString(value); + + } else if (fieldMetaData is SimpleFieldMetadata && (fieldMetaData[FIELD_DATA_TYPE] == ENUM)) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == ENUM) { + return value; + + } else if (fieldMetaData is SimpleFieldMetadata && (fieldMetaData[FIELD_DATA_TYPE] == DATE)) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == DATE) { + return self.stringToTime(value, DATE); + + } else if (fieldMetaData is SimpleFieldMetadata && (fieldMetaData[FIELD_DATA_TYPE] == TIME_OF_DAY)) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == TIME_OF_DAY) { + return self.stringToTime(value, TIME_OF_DAY); + + } else if (fieldMetaData is SimpleFieldMetadata && (fieldMetaData[FIELD_DATA_TYPE] == CIVIL)) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == CIVIL) { + return self.stringToTime(value, CIVIL); + + } else if (fieldMetaData is SimpleFieldMetadata && (fieldMetaData[FIELD_DATA_TYPE] == UTC)) + || (fieldMetaData is EntityFieldMetadata && fieldMetaData[RELATION][REF_FIELD_DATA_TYPE] == UTC) { + return self.stringToTime(value, UTC); + + } else { + return error persist:Error("Unsupported Data Format"); + } + } + + isolated function logQuery(RedisDBOperation msgTag, string|string[]|map metadata) { + + string info = ""; + if metadata is string { + match msgTag { + KEYS => { + info = string `Pattern : ${metadata}`; + } + REDISTYPE|SCARD|SMEMBERS => { + info = string `Key : ${metadata}`; + } + } + } else if metadata is string[] { + match msgTag { + EXISTS|DEL => { + info = string `Keys : ${metadata.toString()}`; + } + } + } else { + match msgTag { + HMSET => { + info = string `Key : ${metadata["key"] ?: ""}, Record : ${metadata["record"] ?: ""}`; + } + SREM => { + info = string `Remove ${metadata["key"] ?: ""} from the set ${metadata["suffixes"] ?: ""}`; + } + HSET => { + info = string `Key : ${metadata["key"] ?: ""}, FieldName : ${metadata["fieldName"] ?: ""}, + New Value : ${metadata["newValue"] ?: ""}`; + } + SADD => { + info = string `Key : ${metadata["key"] ?: ""}, Element : ${metadata["suffixes"] ?: ""}`; + } + HMGET => { + info = string `Key : ${metadata["key"] ?: ""}, FieldNames : ${metadata["fieldNames"] ?: ""}`; + } + } + } + log:printDebug(string ` ${info}`); + } +} diff --git a/ballerina/redis_metadata.bal b/ballerina/redis_metadata.bal new file mode 100644 index 0000000..c5e51b5 --- /dev/null +++ b/ballerina/redis_metadata.bal @@ -0,0 +1,32 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + +# Represents the required DB operations in a `persist:RedisClient`. +# Only used by `persist:RedisClient`. +# +enum RedisDBOperation { + KEYS, + REDISTYPE, + EXISTS, + HMSET, + HMGET, + HSET, + SREM, + DEL, + SCARD, + SADD, + SMEMBERS +} diff --git a/ballerina/stream_types.bal b/ballerina/stream_types.bal new file mode 100644 index 0000000..623c96c --- /dev/null +++ b/ballerina/stream_types.bal @@ -0,0 +1,85 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; + +public class PersistRedisStream { + + private stream? anydataStream; + private persist:Error? err; + private string[] fields; + private string[] include; + private typedesc[] typeDescriptions; + private RedisClient? persistClient; + private typedesc targetType; + private map typeMap; + + public isolated function init(stream? anydataStream, typedesc targetType, map typeMap, string[] fields, string[] include, any[] typeDescriptions, RedisClient persistClient, persist:Error? err = ()) { + self.anydataStream = anydataStream; + self.fields = fields; + self.include = include; + self.targetType = targetType; + self.typeMap = typeMap; + + typedesc[] typeDescriptionsArray = []; + foreach any typeDescription in typeDescriptions { + typeDescriptionsArray.push(>typeDescription); + } + self.typeDescriptions = typeDescriptionsArray; + self.persistClient = persistClient; + self.err = err; + } + + public isolated function next() returns record {|record {} value;|}|persist:Error? { + if self.err is persist:Error { + return self.err; + } else if self.anydataStream is stream { + var anydataStream = >self.anydataStream; + var streamValue = anydataStream.next(); + if streamValue is () { + return streamValue; + } else if streamValue is error { + return error persist:Error(streamValue.message(), streamValue); + } else { + record {}|error value = streamValue.value; + if value is error { + return error persist:Error(value.message(), value); + } + check (self.persistClient).getManyRelations(self.typeMap, value, self.fields, + self.include); + + string[] keyFields = (self.persistClient).getKeyFields(); + foreach string keyField in keyFields { + if self.fields.indexOf(keyField) is () && value.hasKey(keyField) { + _ = value.remove(keyField); + } + } + record {|record {} value;|} nextRecord = {value: checkpanic value.cloneWithType(self.targetType)}; + return nextRecord; + } + } + return (); + } + + public isolated function close() returns persist:Error? { + (stream)? str = self.anydataStream; + if str is stream { + error? e = str.close(); + if e is error { + return error persist:Error(e.message(), e); + } + } + } +} diff --git a/ballerina/tests/Config.toml b/ballerina/tests/Config.toml new file mode 100644 index 0000000..46c1b3c --- /dev/null +++ b/ballerina/tests/Config.toml @@ -0,0 +1,2 @@ +[redis] +connection = "redis://localhost:6379" diff --git a/ballerina/tests/init-test.bal b/ballerina/tests/init-test.bal new file mode 100644 index 0000000..0d50db5 --- /dev/null +++ b/ballerina/tests/init-test.bal @@ -0,0 +1,747 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/time; +import ballerinax/redis; + +configurable record {| + redis:ConnectionUri|redis:ConnectionParams connection?; + boolean connectionPooling = false; + boolean isClusterConnection = false; + redis:SecureSocket secureSocket?; +|} & readonly redis = ?; + +@test:BeforeSuite +function initTests() returns error? { + redis:Client redisDbClient = check new (redis); + _ = check redisDbClient.close(); +} + +AllTypes allTypes1 = { + id: 1, + booleanType: false, + intType: 5, + floatType: 6.0, + decimalType: 23.44, + stringType: "test-2", + dateType: {year: 1993, month: 11, day: 3}, + timeOfDayType: {hour: 12, minute: 32, second: 34}, + utcType: [1684493685, 0.998012], + civilType: {utcOffset: {hours: 5, minutes: 30, seconds: 0}, timeAbbrev: "Asia/Colombo", year: 2024, month: 2, + day: 27, hour: 10, minute: 30, second: 21}, + booleanTypeOptional: false, + intTypeOptional: 5, + floatTypeOptional: 6.0, + decimalTypeOptional: 23.44, + stringTypeOptional: "test", + dateTypeOptional: {year: 1993, month: 11, day: 3}, + timeOfDayTypeOptional: {hour: 12, minute: 32, second: 34}, + utcTypeOptional: [1684493685, 0.998012], + civilTypeOptional: {utcOffset: {hours: 5, minutes: 30, seconds: 0}, timeAbbrev: "Asia/Colombo", year: 2024, + month: 2, day: 27, hour: 10, minute: 30, second: 21}, + enumType: "TYPE_3", + enumTypeOptional: "TYPE_2" +}; + +AllTypes allTypes1Expected = { + id: allTypes1.id, + booleanType: allTypes1.booleanType, + intType: allTypes1.intType, + floatType: allTypes1.floatType, + decimalType: allTypes1.decimalType, + stringType: allTypes1.stringType, + dateType: allTypes1.dateType, + timeOfDayType: allTypes1.timeOfDayType, + utcType: allTypes1.utcType, + civilType: allTypes1.civilType, + booleanTypeOptional: allTypes1.booleanTypeOptional, + intTypeOptional: allTypes1.intTypeOptional, + floatTypeOptional: allTypes1.floatTypeOptional, + decimalTypeOptional: allTypes1.decimalTypeOptional, + stringTypeOptional: allTypes1.stringTypeOptional, + dateTypeOptional: allTypes1.dateTypeOptional, + timeOfDayTypeOptional: allTypes1.timeOfDayTypeOptional, + utcTypeOptional: allTypes1.utcTypeOptional, + civilTypeOptional: allTypes1.civilTypeOptional, + enumType: allTypes1.enumType, + enumTypeOptional: allTypes1.enumTypeOptional +}; + +AllTypes allTypes2 = { + id: 2, + booleanType: true, + intType: 34, + floatType: 63.0, + decimalType: 233.44, + stringType: "test2", + dateType: {year: 1996, month: 11, day: 3}, + timeOfDayType: {hour: 17, minute: 32, second: 34}, + utcType: [1684493685, 0.998012], + civilType: {utcOffset: {hours: 5, minutes: 30, seconds: 0}, timeAbbrev: "Asia/Colombo", year: 2024, month: 2, + day: 27, hour: 10, minute: 30, second: 21}, + booleanTypeOptional: true, + intTypeOptional: 6, + floatTypeOptional: 66.0, + decimalTypeOptional: 233.44, + stringTypeOptional: "test2", + dateTypeOptional: {year: 1293, month: 11, day: 3}, + timeOfDayTypeOptional: {hour: 19, minute: 32, second: 34}, + utcTypeOptional: [1684493685, 0.998012], + civilTypeOptional: {utcOffset: {hours: 5, minutes: 30, seconds: 0}, timeAbbrev: "Asia/Colombo", year: 2024, + month: 2, day: 27, hour: 10, minute: 30, second: 21}, + enumType: "TYPE_1", + enumTypeOptional: "TYPE_3" +}; + +AllTypes allTypes2Expected = { + id: allTypes2.id, + booleanType: allTypes2.booleanType, + intType: allTypes2.intType, + floatType: allTypes2.floatType, + decimalType: allTypes2.decimalType, + stringType: allTypes2.stringType, + dateType: allTypes2.dateType, + timeOfDayType: allTypes2.timeOfDayType, + utcType: allTypes2.utcType, + civilType: allTypes2.civilType, + booleanTypeOptional: allTypes2.booleanTypeOptional, + intTypeOptional: allTypes2.intTypeOptional, + floatTypeOptional: allTypes2.floatTypeOptional, + decimalTypeOptional: allTypes2.decimalTypeOptional, + stringTypeOptional: allTypes2.stringTypeOptional, + dateTypeOptional: allTypes2.dateTypeOptional, + timeOfDayTypeOptional: allTypes2.timeOfDayTypeOptional, + utcTypeOptional: allTypes2.utcTypeOptional, + civilTypeOptional: allTypes2.civilTypeOptional, + enumType: allTypes2.enumType, + enumTypeOptional: allTypes2.enumTypeOptional +}; + +AllTypes allTypes3 = { + id: 3, + booleanType: true, + intType: 35, + floatType: 63.0, + decimalType: 233.44, + stringType: "test2", + dateType: {year: 1996, month: 11, day: 3}, + timeOfDayType: {hour: 17, minute: 32, second: 34}, + utcType: [1684493685, 0.998012], + civilType: {utcOffset: {hours: 5, minutes: 30, seconds: 0}, timeAbbrev: "Asia/Colombo", year: 2024, month: 2, + day: 27, hour: 10, minute: 30, second: 21}, + enumType: "TYPE_1" +}; + +AllTypes allTypes3Expected = { + id: allTypes3.id, + booleanType: allTypes3.booleanType, + intType: allTypes3.intType, + floatType: allTypes3.floatType, + decimalType: allTypes3.decimalType, + stringType: allTypes3.stringType, + dateType: allTypes3.dateType, + timeOfDayType: allTypes3.timeOfDayType, + utcType: allTypes3.utcType, + civilType: allTypes3.civilType, + enumType: allTypes3.enumType +}; + +AllTypes allTypes1Updated = { + id: 1, + booleanType: true, + intType: 99, + floatType: 63.0, + decimalType: 53.44, + stringType: "testUpdate", + dateType: {year: 1996, month: 12, day: 13}, + timeOfDayType: {hour: 16, minute: 12, second: 14}, + utcType: [1686493685, 0.996012], + civilType: {utcOffset: {hours: 6, minutes: 0, seconds: 0}, timeAbbrev: "Asia/Colombo", year: 2022, month: 12, + day: 7, hour: 14, minute: 5, second: 43}, + booleanTypeOptional: true, + intTypeOptional: 53, + floatTypeOptional: 26.0, + decimalTypeOptional: 223.44, + stringTypeOptional: "testUpdate", + dateTypeOptional: {year: 1923, month: 11, day: 3}, + timeOfDayTypeOptional: {hour: 18, minute: 32, second: 34}, + utcTypeOptional: [1686493685, 0.996012], + civilTypeOptional: {utcOffset: {hours: 6, minutes: 0, seconds: 0}, timeAbbrev: "Asia/Colombo", year: 2022, + month: 12, day: 7, hour: 14, minute: 5, second: 43}, + enumType: "TYPE_4", + enumTypeOptional: "TYPE_4" +}; + +AllTypes allTypes1UpdatedExpected = { + id: allTypes1Updated.id, + booleanType: allTypes1Updated.booleanType, + intType: allTypes1Updated.intType, + floatType: allTypes1Updated.floatType, + decimalType: allTypes1Updated.decimalType, + stringType: allTypes1Updated.stringType, + dateType: allTypes1Updated.dateType, + timeOfDayType: allTypes1Updated.timeOfDayType, + utcType: allTypes1Updated.utcType, + civilType: allTypes1Updated.civilType, + booleanTypeOptional: allTypes1Updated.booleanTypeOptional, + intTypeOptional: allTypes1Updated.intTypeOptional, + floatTypeOptional: allTypes1Updated.floatTypeOptional, + decimalTypeOptional: allTypes1Updated.decimalTypeOptional, + stringTypeOptional: allTypes1Updated.stringTypeOptional, + dateTypeOptional: allTypes1Updated.dateTypeOptional, + timeOfDayTypeOptional: allTypes1Updated.timeOfDayTypeOptional, + utcTypeOptional: allTypes1Updated.utcTypeOptional, + civilTypeOptional: allTypes1Updated.civilTypeOptional, + enumType: allTypes1Updated.enumType, + enumTypeOptional: allTypes1Updated.enumTypeOptional +}; + +public type AllTypesDependent record {| + boolean booleanType; + int intType; + float floatType; + decimal decimalType; + string stringType; + time:Date dateType; + time:TimeOfDay timeOfDayType; + time:Utc utcType; + time:Civil civilType; + boolean booleanTypeOptional?; + int intTypeOptional?; + float floatTypeOptional?; + decimal decimalTypeOptional?; + string stringTypeOptional?; + time:Date dateTypeOptional?; + time:TimeOfDay timeOfDayTypeOptional?; + time:Utc utcTypeOptional?; + time:Civil civilTypeOptional?; +|}; + +OrderItemExtended orderItemExtended1 = { + orderId: "order-1", + itemId: "item-1", + CustomerId: 1, + paid: false, + ammountPaid: 10.5f, + ammountPaidDecimal: 10.5, + arivalTimeDate: {year: 2021, month: 4, day: 12}, + arivalTimeTimeOfDay: {hour: 17, minute: 50, second: 50.52}, + orderType: INSTORE +}; + +OrderItemExtended orderItemExtendedRetrieved = { + orderId: "order-1", + itemId: "item-1", + CustomerId: 1, + paid: false, + ammountPaid: 10.5f, + ammountPaidDecimal: 10.5, + arivalTimeDate: {year: 2021, month: 4, day: 12}, + arivalTimeTimeOfDay: {hour: 17, minute: 50, second: 50.52}, + orderType: INSTORE +}; + +OrderItemExtended orderItemExtended2 = { + orderId: "order-2", + itemId: "item-2", + CustomerId: 1, + paid: false, + ammountPaid: 10.5f, + ammountPaidDecimal: 10.5, + arivalTimeDate: {year: 2021, month: 4, day: 12}, + arivalTimeTimeOfDay: {hour: 17, minute: 50, second: 50.52}, + orderType: ONLINE +}; + +public type EmployeeInfo record {| + string firstName; + string lastName; + record {| + string deptName; + |} department; + Workspace workspace?; +|}; + +OrderItemExtended orderItemExtended2Retrieved = { + orderId: "order-2", + itemId: "item-2", + CustomerId: 1, + paid: false, + ammountPaid: 10.5f, + ammountPaidDecimal: 10.5, + arivalTimeDate: {year: 2021, month: 4, day: 12}, + arivalTimeTimeOfDay: {hour: 17, minute: 50, second: 50.52}, + orderType: ONLINE +}; + +OrderItemExtended orderItemExtended3 = { + orderId: "order-3", + itemId: "item-3", + CustomerId: 4, + paid: true, + ammountPaid: 20.5f, + ammountPaidDecimal: 20.5, + arivalTimeDate: {year: 2021, month: 4, day: 12}, + arivalTimeTimeOfDay: {hour: 17, minute: 50, second: 50.52}, + orderType: INSTORE +}; + +OrderItemExtended orderItemExtended3Retrieved = { + orderId: "order-2", + itemId: "item-2", + CustomerId: 1, + paid: true, + ammountPaid: 10.5f, + ammountPaidDecimal: 10.5, + arivalTimeDate: {year: 2021, month: 4, day: 12}, + arivalTimeTimeOfDay: {hour: 17, minute: 50, second: 50.52}, + orderType: ONLINE +}; + +public type DepartmentInfo record {| + string deptNo; + string deptName; + record {| + string firstName; + string lastName; + |}[] employees; +|}; + +public type WorkspaceInfo record {| + string workspaceType; + Building location; + Employee[] employees; +|}; + +public type BuildingInfo record {| + string buildingCode; + string city; + string state; + string country; + string postalCode; + string 'type; + Workspace[] workspaces; +|}; + +Building building1 = { + buildingCode: "building-1", + city: "Colombo", + state: "Western Province", + country: "Sri Lanka", + postalCode: "10370", + 'type: "rented" +}; + +Building invalidBuilding = { + buildingCode: "building-invalid-extra-characters-to-force-failure", + city: "Colombo", + state: "Western Province", + country: "Sri Lanka", + postalCode: "10370", + 'type: "owned" +}; + +BuildingInsert building2 = { + buildingCode: "building-2", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" +}; + +BuildingInsert building3 = { + buildingCode: "building-3", + city: "London", + state: "London", + country: "United Kingdom", + postalCode: "39202", + 'type: "rented" +}; + +Building updatedBuilding1 = { + buildingCode: "building-1", + city: "Galle", + state: "Southern Province", + country: "Sri Lanka", + postalCode: "10890", + 'type: "owned" +}; + +Department department1 = { + deptNo: "department-1", + deptName: "Finance" +}; + +Department invalidDepartment = { + deptNo: "invalid-department-extra-characters-to-force-failure", + deptName: "Finance" +}; + +Department department2 = { + deptNo: "department-2", + deptName: "Marketing" +}; + +Department department3 = { + deptNo: "department-3", + deptName: "Engineering" +}; + +Department updatedDepartment1 = { + deptNo: "department-1", + deptName: "Finance & Legalities" +}; + +Employee employee1 = { + empNo: "employee-1", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-2", + workspaceWorkspaceId: "workspace-2" +}; + +Employee invalidEmployee = { + empNo: "invalid-employee-no-extra-characters-to-force-failure", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-2", + workspaceWorkspaceId: "workspace-2" +}; + +Employee employee2 = { + empNo: "employee-2", + firstName: "Jane", + lastName: "Doe", + birthDate: {year: 1996, month: 9, day: 15}, + gender: FEMALE, + hireDate: {year: 2022, month: 6, day: 1}, + departmentDeptNo: "department-2", + workspaceWorkspaceId: "workspace-2" +}; + +Employee employee3 = { + empNo: "employee-3", + firstName: "Hugh", + lastName: "Smith", + birthDate: {year: 1986, month: 9, day: 15}, + gender: FEMALE, + hireDate: {year: 2021, month: 6, day: 1}, + departmentDeptNo: "department-3", + workspaceWorkspaceId: "workspace-3" +}; + +Employee updatedEmployee1 = { + empNo: "employee-1", + firstName: "Tom", + lastName: "Jones", + birthDate: {year: 1994, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-3", + workspaceWorkspaceId: "workspace-2" +}; + +public type IntIdRecordDependent record {| + string randomField; +|}; + +public type StringIdRecordDependent record {| + string randomField; +|}; + +public type FloatIdRecordDependent record {| + string randomField; +|}; + +public type DecimalIdRecordDependent record {| + string randomField; +|}; + +public type BooleanIdRecordDependent record {| + string randomField; +|}; + +public type AllTypesIdRecordDependent record {| + string randomField; +|}; + +public type CompositeAssociationRecordDependent record {| + string randomField; + int alltypesidrecordIntType; + decimal alltypesidrecordDecimalType; + record {| + int intType; + string stringType; + boolean booleanType; + string randomField; + |} allTypesIdRecord; +|}; + +Workspace workspace1 = { + workspaceId: "workspace-1", + workspaceType: "small", + locationBuildingCode: "building-2" +}; + +Workspace invalidWorkspace = { + workspaceId: "invalid-workspace-extra-characters-to-force-failure", + workspaceType: "small", + locationBuildingCode: "building-2" +}; + +Workspace workspace2 = { + workspaceId: "workspace-2", + workspaceType: "medium", + locationBuildingCode: "building-2" +}; + +Workspace workspace3 = { + workspaceId: "workspace-3", + workspaceType: "large", + locationBuildingCode: "building-2" +}; + +Workspace updatedWorkspace1 = { + workspaceId: "workspace-1", + workspaceType: "large", + locationBuildingCode: "building-2" +}; + +public type EmployeeName record {| + string firstName; + string lastName; +|}; + +public type EmployeeInfo2 record {| + readonly string empNo; + time:Date birthDate; + string departmentDeptNo; + string workspaceWorkspaceId; +|}; + +public type WorkspaceInfo2 record {| + string workspaceType; + string locationBuildingCode; +|}; + +public type DepartmentInfo2 record {| + string deptName; +|}; + +public type BuildingInfo2 record {| + string city; + string state; + string country; + string postalCode; + string 'type; +|}; + +OrderItem orderItem1 = { + orderId: "order-1", + itemId: "item-1", + quantity: 5, + notes: "none" +}; + +OrderItem orderItem2 = { + orderId: "order-2", + itemId: "item-2", + quantity: 10, + notes: "more" +}; + +OrderItem orderItem2Updated = { + orderId: "order-2", + itemId: "item-2", + quantity: 20, + notes: "more than more" +}; + +Building building31 = { + buildingCode: "building-31", + city: "Colombo", + state: "Western Province", + country: "Sri Lanka", + postalCode: "10370", + 'type: "rented" +}; + +BuildingInsert building32 = { + buildingCode: "building-32", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" +}; + +BuildingInsert building33 = { + buildingCode: "building-33", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" +}; + +Building building33Updated = { + buildingCode: "building-33", + city: "ColomboUpdated", + state: "Western ProvinceUpdated", + country: "Sri LankaUpdated", + postalCode: "10570", + 'type: "owned" +}; + +Department departmentNative1 = { + deptNo: "department-native-1", + deptName: "Finance" +}; + +Department departmentNative2 = { + deptNo: "department-native-2", + deptName: "HR" +}; + +Department departmentNative3 = { + deptNo: "department-native-3", + deptName: "Marketing" +}; + +Building buildingNative1 = { + buildingCode: "building-native-1", + city: "Colombo", + state: "Western", + country: "Sri Lanka", + postalCode: "10370", + 'type: "office" +}; + +Building buildingNative2 = { + buildingCode: "building-native-2", + city: "Kandy", + state: "Central", + country: "Sri Lanka", + postalCode: "20000", + 'type: "coworking space" +}; + +Building buildingNative3 = { + buildingCode: "building-native-3", + city: "San Francisco", + state: "California", + country: "USA", + postalCode: "80000", + 'type: "office" +}; + +Workspace workspaceNative1 = { + workspaceId: "workspace-native-1", + workspaceType: "hot seat", + locationBuildingCode: "building-native-2" +}; + +Workspace workspaceNative2 = { + workspaceId: "workspace-native-2", + workspaceType: "dedicated", + locationBuildingCode: "building-native-2" +}; + +Workspace workspaceNative3 = { + workspaceId: "workspace-native-3", + workspaceType: "hot seat", + locationBuildingCode: "building-native-3" +}; + +Employee employeeNative1 = { + empNo: "employee-native-1", + firstName: "John", + lastName: "Doe", + birthDate: {year: 1994, month: 10, day: 30}, + gender: MALE, + hireDate: {year: 2020, month: 10, day: 30}, + departmentDeptNo: "department-native-1", + workspaceWorkspaceId: "workspace-native-1" +}; + +Employee employeeNative2 = { + empNo: "employee-native-2", + firstName: "Jane", + lastName: "Doe", + birthDate: {year: 1996, month: 8, day: 12}, + gender: FEMALE, + hireDate: {year: 2021, month: 10, day: 30}, + departmentDeptNo: "department-native-2", + workspaceWorkspaceId: "workspace-native-2" +}; + +Employee employeeNative3 = { + empNo: "employee-native-3", + firstName: "Sam", + lastName: "Smith", + birthDate: {year: 1991, month: 8, day: 12}, + gender: MALE, + hireDate: {year: 2019, month: 10, day: 30}, + departmentDeptNo: "department-native-3", + workspaceWorkspaceId: "workspace-native-3" +}; + +EmployeeInfo employeeInfoNative1 = { + firstName: "John", + lastName: "Doe", + department: { + deptName: "Finance" + }, + workspace: { + workspaceId: "workspace-native-1", + workspaceType: "hot seat", + locationBuildingCode: "building-native-2" + } +}; + +EmployeeInfo employeeInfoNative2 = { + firstName: "Jane", + lastName: "Doe", + department: { + deptName: "HR" + }, + workspace: { + workspaceId: "workspace-native-2", + workspaceType: "dedicated", + locationBuildingCode: "building-native-2" + } +}; + +EmployeeInfo employeeInfoNative3 = { + firstName: "Sam", + lastName: "Smith", + department: { + deptName: "Marketing" + }, + workspace: { + workspaceId: "workspace-native-3", + workspaceType: "hot seat", + locationBuildingCode: "building-native-3" + } +}; diff --git a/ballerina/tests/persist/rainier.bal b/ballerina/tests/persist/rainier.bal new file mode 100644 index 0000000..34b9a0e --- /dev/null +++ b/ballerina/tests/persist/rainier.bal @@ -0,0 +1,67 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/time; + +enum Gender { + MALE, + FEMALE +} + +type Employee record {| + readonly string empNo; + string firstName; + string lastName; + time:Date birthDate; + Gender gender; + time:Date hireDate; + + Department department; + Workspace workspace; +|}; + +type Workspace record {| + readonly string workspaceId; + string workspaceType; + + Building location; + Employee[] employees; +|}; + +type Building record {| + readonly string buildingCode; + string city; + string state; + string country; + string postalCode; + string 'type; + + Workspace[] workspaces; +|}; + +type Department record {| + readonly string deptNo; + string deptName; + + Employee[] employees; +|}; + +type OrderItem record {| + readonly string orderId; + readonly string itemId; + int quantity; + string notes; +|}; diff --git a/ballerina/tests/persist/test_entities.bal b/ballerina/tests/persist/test_entities.bal new file mode 100644 index 0000000..27663c7 --- /dev/null +++ b/ballerina/tests/persist/test_entities.bal @@ -0,0 +1,89 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/time; + +enum EnumType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4 +} + +type AllTypes record {| + readonly int id; + boolean booleanType; + int intType; + float floatType; + decimal decimalType; + string stringType; + time:Date dateType; + time:TimeOfDay timeOfDayType; + time:Utc utcType; + time:Civil civilType; + EnumType enumType; + boolean booleanTypeOptional?; + int intTypeOptional?; + float floatTypeOptional?; + decimal decimalTypeOptional?; + string stringTypeOptional?; + time:Date dateTypeOptional?; + time:TimeOfDay timeOfDayTypeOptional?; + time:Utc utcTypeOptional?; + time:Civil civilTypeOptional?; + EnumType enumTypeOptional?; +|}; + +type StringIdRecord record {| + readonly string id; + string randomField; +|}; + +type IntIdRecord record {| + readonly int id; + string randomField; +|}; + +type FloatIdRecord record {| + readonly float id; + string randomField; +|}; + +type DecimalIdRecord record {| + readonly decimal id; + string randomField; +|}; + +type BooleanIdRecord record {| + readonly boolean id; + string randomField; +|}; + +type CompositeAssociationRecord record {| + readonly string id; + string randomField; + AllTypesIdRecord allTypesIdRecord; +|}; + +type AllTypesIdRecord record {| + readonly boolean booleanType; + readonly int intType; + readonly float floatType; + readonly decimal decimalType; + readonly string stringType; + string randomField; + CompositeAssociationRecord? compositeAssociationRecord; +|}; diff --git a/ballerina/tests/rainier_generated_types.bal b/ballerina/tests/rainier_generated_types.bal new file mode 100644 index 0000000..2409aa2 --- /dev/null +++ b/ballerina/tests/rainier_generated_types.bal @@ -0,0 +1,172 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/time; + +public enum Gender { + MALE, + FEMALE +} + +public type Employee record {| + readonly string empNo; + string firstName; + string lastName; + time:Date birthDate; + Gender gender; + time:Date hireDate; + string departmentDeptNo; + string workspaceWorkspaceId; +|}; + +public type EmployeeOptionalized record {| + string empNo?; + string firstName?; + string lastName?; + time:Date birthDate?; + Gender gender?; + time:Date hireDate?; + string departmentDeptNo?; + string workspaceWorkspaceId?; +|}; + +public type EmployeeWithRelations record {| + *EmployeeOptionalized; + DepartmentOptionalized department?; + WorkspaceOptionalized workspace?; +|}; + +public type EmployeeTargetType typedesc; + +public type EmployeeInsert Employee; + +public type EmployeeUpdate record {| + string firstName?; + string lastName?; + time:Date birthDate?; + Gender gender?; + time:Date hireDate?; + string departmentDeptNo?; + string workspaceWorkspaceId?; +|}; + +public type Workspace record {| + readonly string workspaceId; + string workspaceType; + string locationBuildingCode; +|}; + +public type WorkspaceOptionalized record {| + string workspaceId?; + string workspaceType?; + string locationBuildingCode?; +|}; + +public type WorkspaceWithRelations record {| + *WorkspaceOptionalized; + BuildingOptionalized location?; + EmployeeOptionalized[] employees?; +|}; + +public type WorkspaceTargetType typedesc; + +public type WorkspaceInsert Workspace; + +public type WorkspaceUpdate record {| + string workspaceType?; + string locationBuildingCode?; +|}; + +public type Building record {| + readonly string buildingCode; + string city; + string state; + string country; + string postalCode; + string 'type; +|}; + +public type BuildingOptionalized record {| + string buildingCode?; + string city?; + string state?; + string country?; + string postalCode?; + string 'type?; +|}; + +public type BuildingWithRelations record {| + *BuildingOptionalized; + WorkspaceOptionalized[] workspaces?; +|}; + +public type BuildingTargetType typedesc; + +public type BuildingInsert Building; + +public type BuildingUpdate record {| + string city?; + string state?; + string country?; + string postalCode?; + string 'type?; +|}; + +public type Department record {| + readonly string deptNo; + string deptName; +|}; + +public type DepartmentOptionalized record {| + string deptNo?; + string deptName?; +|}; + +public type DepartmentWithRelations record {| + *DepartmentOptionalized; + EmployeeOptionalized[] employees?; +|}; + +public type DepartmentTargetType typedesc; + +public type DepartmentInsert Department; + +public type DepartmentUpdate record {| + string deptName?; +|}; + +public type OrderItem record {| + readonly string orderId; + readonly string itemId; + int quantity; + string notes; +|}; + +public type OrderItemOptionalized record {| + string orderId?; + string itemId?; + int quantity?; + string notes?; +|}; + +public type OrderItemTargetType typedesc; + +public type OrderItemInsert OrderItem; + +public type OrderItemUpdate record {| + int quantity?; + string notes?; +|}; diff --git a/ballerina/tests/redis-all-types-tests.bal b/ballerina/tests/redis-all-types-tests.bal new file mode 100644 index 0000000..d63a52f --- /dev/null +++ b/ballerina/tests/redis-all-types-tests.bal @@ -0,0 +1,224 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; +import ballerina/test; + +@test:Config { + groups: ["all-types", "redis"] +} +function redisAllTypesCreateTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + int[] ids = check testEntitiesClient->/alltypes.post([allTypes1, allTypes2]); + test:assertEquals(ids, [allTypes1.id, allTypes2.id]); + + AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes1.id]; + test:assertEquals(allTypesRetrieved, allTypes1Expected); + + allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes2.id]; + test:assertEquals(allTypesRetrieved, allTypes2Expected); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["all-types", "redis"] +} +function redisAllTypesCreateOptionalTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + int[] ids = check testEntitiesClient->/alltypes.post([allTypes3]); + test:assertEquals(ids, [allTypes3.id]); + + AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes3.id]; + test:assertEquals(allTypesRetrieved, allTypes3Expected); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["all-types", "redis"], + dependsOn: [redisAllTypesCreateTest, redisAllTypesCreateOptionalTest] +} +function redisAllTypesReadTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + stream allTypesStream = testEntitiesClient->/alltypes; + AllTypes[] allTypes = check from AllTypes allTypesRecord in allTypesStream order by allTypesRecord.id ascending + select allTypesRecord; + + test:assertEquals(allTypes, [allTypes1Expected, allTypes2Expected, allTypes3Expected]); + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["all-types", "redis", "dependent"], + dependsOn: [redisAllTypesCreateTest, redisAllTypesCreateOptionalTest] +} +function redisAllTypesReadDependentTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + stream allTypesStream = testEntitiesClient->/alltypes; + AllTypesDependent[] allTypes = check from AllTypesDependent allTypesRecord in allTypesStream order by + allTypesRecord.intType ascending select allTypesRecord; + + test:assertEquals(allTypes, [ + { + booleanType: allTypes1Expected.booleanType, + intType: allTypes1Expected.intType, + floatType: allTypes1Expected.floatType, + decimalType: allTypes1Expected.decimalType, + stringType: allTypes1Expected.stringType, + dateType: allTypes1Expected.dateType, + timeOfDayType: allTypes1Expected.timeOfDayType, + utcType: allTypes1Expected.utcType, + civilType: allTypes1Expected.civilType, + booleanTypeOptional: allTypes1Expected.booleanTypeOptional, + intTypeOptional: allTypes1Expected.intTypeOptional, + floatTypeOptional: allTypes1Expected.floatTypeOptional, + decimalTypeOptional: allTypes1Expected.decimalTypeOptional, + stringTypeOptional: allTypes1Expected.stringTypeOptional, + dateTypeOptional: allTypes1Expected.dateTypeOptional, + timeOfDayTypeOptional: allTypes1Expected.timeOfDayTypeOptional, + utcTypeOptional: allTypes1Expected.utcTypeOptional, + civilTypeOptional: allTypes1Expected.civilTypeOptional + }, + { + booleanType: allTypes2Expected.booleanType, + intType: allTypes2Expected.intType, + floatType: allTypes2Expected.floatType, + decimalType: allTypes2Expected.decimalType, + stringType: allTypes2Expected.stringType, + dateType: allTypes2Expected.dateType, + timeOfDayType: allTypes2Expected.timeOfDayType, + utcType: allTypes2Expected.utcType, + civilType: allTypes2Expected.civilType, + booleanTypeOptional: allTypes2Expected.booleanTypeOptional, + intTypeOptional: allTypes2Expected.intTypeOptional, + floatTypeOptional: allTypes2Expected.floatTypeOptional, + decimalTypeOptional: allTypes2Expected.decimalTypeOptional, + stringTypeOptional: allTypes2Expected.stringTypeOptional, + dateTypeOptional: allTypes2Expected.dateTypeOptional, + timeOfDayTypeOptional: allTypes2Expected.timeOfDayTypeOptional, + utcTypeOptional: allTypes2Expected.utcTypeOptional, + civilTypeOptional: allTypes2Expected.civilTypeOptional + }, + { + booleanType: allTypes3Expected.booleanType, + intType: allTypes3Expected.intType, + floatType: allTypes3Expected.floatType, + decimalType: allTypes3Expected.decimalType, + stringType: allTypes3Expected.stringType, + dateType: allTypes3Expected.dateType, + timeOfDayType: allTypes3Expected.timeOfDayType, + utcType: allTypes3Expected.utcType, + civilType: allTypes3Expected.civilType + } + ]); + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["all-types", "redis"], + dependsOn: [redisAllTypesCreateTest, redisAllTypesCreateOptionalTest] +} +function redisAllTypesReadOneTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes1.id]; + test:assertEquals(allTypesRetrieved, allTypes1Expected); + + allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes2.id]; + test:assertEquals(allTypesRetrieved, allTypes2Expected); + + allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes3.id]; + test:assertEquals(allTypesRetrieved, allTypes3Expected); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["all-types", "redis"] +} +function redisAllTypesReadOneTestNegative() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + AllTypes|persist:Error allTypesRetrieved = testEntitiesClient->/alltypes/[4]; + if allTypesRetrieved is persist:NotFoundError { + test:assertEquals(allTypesRetrieved.message(), + "A record with the key 'AllTypes:4' does not exist for the entity 'AllTypes'."); + } + else { + test:assertFail("persist:NotFoundError expected."); + } + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["all-types", "redis"], + dependsOn: [redisAllTypesReadOneTest, redisAllTypesReadTest, redisAllTypesReadDependentTest] +} +function redisAllTypesUpdateTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + AllTypes allTypes = check testEntitiesClient->/alltypes/[allTypes1.id].put({ + booleanType: allTypes3.booleanType, + intType: allTypes1Updated.intType, + floatType: allTypes1Updated.floatType, + decimalType: allTypes1Updated.decimalType, + stringType: allTypes1Updated.stringType, + dateType: allTypes1Updated.dateType, + timeOfDayType: allTypes1Updated.timeOfDayType, + utcType: allTypes1Updated.utcType, + civilType: allTypes1Updated.civilType, + booleanTypeOptional: allTypes1Updated.booleanTypeOptional, + intTypeOptional: allTypes1Updated.intTypeOptional, + floatTypeOptional: allTypes1Updated.floatTypeOptional, + decimalTypeOptional: allTypes1Updated.decimalTypeOptional, + stringTypeOptional: allTypes1Updated.stringTypeOptional, + dateTypeOptional: allTypes1Updated.dateTypeOptional, + timeOfDayTypeOptional: allTypes1Updated.timeOfDayTypeOptional, + utcTypeOptional: allTypes1Updated.utcTypeOptional, + civilTypeOptional: allTypes1Updated.civilTypeOptional, + enumType: allTypes1Updated.enumType, + enumTypeOptional: allTypes1Updated.enumTypeOptional + }); + test:assertEquals(allTypes, allTypes1UpdatedExpected); + + AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes1.id]; + test:assertEquals(allTypesRetrieved, allTypes1UpdatedExpected); + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["all-types", "redis"], + dependsOn: [redisAllTypesUpdateTest] +} +function redisAllTypesDeleteTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + AllTypes allTypes = check testEntitiesClient->/alltypes/[allTypes2.id].delete(); + test:assertEquals(allTypes, allTypes2Expected); + + stream allTypesStream = testEntitiesClient->/alltypes; + AllTypes[] allTypesCollection = check from AllTypes allTypesRecord in allTypesStream order by allTypesRecord.id + ascending select allTypesRecord; + + test:assertEquals(allTypesCollection, [allTypes1UpdatedExpected, allTypes3Expected]); + check testEntitiesClient.close(); +} diff --git a/ballerina/tests/redis-associations-tests.bal b/ballerina/tests/redis-associations-tests.bal new file mode 100644 index 0000000..c74f2f3 --- /dev/null +++ b/ballerina/tests/redis-associations-tests.bal @@ -0,0 +1,288 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; +import ballerina/test; + +@test:Config { + groups: ["associations", "redis"], + dependsOn: [redisEmployeeDeleteTestNegative] +} +function redisEmployeeRelationsTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee employee21 = { + empNo: "employee-21", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + }; + + Workspace workspace22 = { + workspaceId: "workspace-22", + workspaceType: "medium", + locationBuildingCode: "building-22" + }; + + BuildingInsert building22 = { + buildingCode: "building-22", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" + }; + + Department department22 = { + deptNo: "department-22", + deptName: "Marketing" + }; + + _ = check rainierClient->/buildings.post([building22]); + _ = check rainierClient->/departments.post([department22]); + _ = check rainierClient->/workspaces.post([workspace22]); + _ = check rainierClient->/employees.post([employee21]); + + stream employeeStream = rainierClient->/employees; + EmployeeInfo[] employees = check from EmployeeInfo employee in employeeStream + select employee; + + EmployeeInfo retrieved = check rainierClient->/employees/["employee-21"]; + + EmployeeInfo expected = { + firstName: "Tom", + lastName: "Scott", + department: { + deptName: "Marketing" + }, + workspace: { + workspaceId: "workspace-22", + workspaceType: "medium", + locationBuildingCode: "building-22" + } + }; + + test:assertEquals(retrieved, expected); + test:assertTrue(employees.indexOf(expected) is int, "Expected EmployeeInfo not found."); + check rainierClient.close(); +} + +@test:Config { + groups: ["associations", "redis"], + dependsOn: [redisEmployeeDeleteTestNegative] +} +function redisDepartmentRelationsTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee employee11 = { + empNo: "employee-11", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-12", + workspaceWorkspaceId: "workspace-12" + }; + + Employee employee12 = { + empNo: "employee-12", + firstName: "Jane", + lastName: "Doe", + birthDate: {year: 1996, month: 9, day: 15}, + gender: FEMALE, + hireDate: {year: 2022, month: 6, day: 1}, + departmentDeptNo: "department-12", + workspaceWorkspaceId: "workspace-12" + }; + + Workspace workspace12 = { + workspaceId: "workspace-12", + workspaceType: "medium", + locationBuildingCode: "building-12" + }; + + BuildingInsert building12 = { + buildingCode: "building-12", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" + }; + + Department department12 = { + deptNo: "department-12", + deptName: "Marketing" + }; + + _ = check rainierClient->/buildings.post([building12]); + _ = check rainierClient->/departments.post([department12]); + _ = check rainierClient->/workspaces.post([workspace12]); + _ = check rainierClient->/employees.post([employee11, employee12]); + + stream departmentStream = rainierClient->/departments; + DepartmentInfo[] departments = check from DepartmentInfo department in departmentStream + select department; + + DepartmentInfo retrieved = check rainierClient->/departments/["department-12"]; + + DepartmentInfo expected = { + deptNo: "department-12", + deptName: "Marketing", + employees: [ + { + firstName: "Tom", + lastName: "Scott" + }, + { + firstName: "Jane", + lastName: "Doe" + } + ] + }; + + test:assertTrue(departments.indexOf(expected) is int, "Expected DepartmentInfo not found."); + test:assertEquals(retrieved, expected); + check rainierClient.close(); +} + +@test:Config { + groups: ["associations", "redis"], + dependsOn: [redisEmployeeRelationsTest] +} +function redisWorkspaceRelationsTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee employee22 = { + empNo: "employee-22", + firstName: "James", + lastName: "David", + birthDate: {year: 1996, month: 11, day: 13}, + gender: FEMALE, + hireDate: {year: 2021, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + }; + _ = check rainierClient->/employees.post([employee22]); + + stream workspaceStream = rainierClient->/workspaces; + WorkspaceInfo[] workspaces = check from WorkspaceInfo workspace in workspaceStream + select workspace; + + WorkspaceInfo retrieved = check rainierClient->/workspaces/["workspace-22"]; + + WorkspaceInfo expected = { + workspaceType: "medium", + location: { + buildingCode: "building-22", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" + }, + employees: [ + { + empNo: "employee-21", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + }, + { + empNo: "employee-22", + firstName: "James", + lastName: "David", + birthDate: {year: 1996, month: 11, day: 13}, + gender: FEMALE, + hireDate: {year: 2021, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + } + ] + }; + + boolean found = false; + _ = from WorkspaceInfo workspace in workspaces + do { + if workspace == expected { + found = true; + } + }; + + if !found { + test:assertFail("Expected WorkspaceInfo not found."); + } + + test:assertEquals(retrieved, expected); + + check rainierClient.close(); +} + +@test:Config { + groups: ["associations", "redis"], + dependsOn: [redisEmployeeRelationsTest] +} +function redisBuildingRelationsTest() returns error? { + RedisRainierClient rainierClient = check new (); + + stream buildingStream = rainierClient->/buildings; + BuildingInfo[] buildings = check from BuildingInfo building in buildingStream + select building; + + BuildingInfo retrieved = check rainierClient->/buildings/["building-22"]; + + BuildingInfo expected = { + buildingCode: "building-22", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned", + workspaces: [ + { + workspaceId: "workspace-22", + workspaceType: "medium", + locationBuildingCode: "building-22" + } + ] + }; + + boolean found = false; + _ = from BuildingInfo building in buildings + do { + if (building.buildingCode == "building-22") { + found = true; + test:assertEquals(building, expected); + } + }; + + if !found { + test:assertFail("Expected BuildingInfo not found."); + } + + test:assertEquals(retrieved, expected); + + check rainierClient.close(); +} diff --git a/ballerina/tests/redis-building-tests.bal b/ballerina/tests/redis-building-tests.bal new file mode 100644 index 0000000..a772dd1 --- /dev/null +++ b/ballerina/tests/redis-building-tests.bal @@ -0,0 +1,196 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; +import ballerina/test; + +@test:Config { + groups: ["building", "redis"] +} +function redisBuildingCreateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] buildingCodes = check rainierClient->/buildings.post([building1]); + test:assertEquals(buildingCodes, [building1.buildingCode]); + + Building buildingRetrieved = check rainierClient->/buildings/[building1.buildingCode]; + test:assertEquals(buildingRetrieved, building1); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"] +} +function redisBuildingCreateTest2() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] buildingCodes = check rainierClient->/buildings.post([building2, building3]); + + test:assertEquals(buildingCodes, [building2.buildingCode, building3.buildingCode]); + + Building buildingRetrieved = check rainierClient->/buildings/[building2.buildingCode]; + test:assertEquals(buildingRetrieved, building2); + + buildingRetrieved = check rainierClient->/buildings/[building3.buildingCode]; + test:assertEquals(buildingRetrieved, building3); + + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"], + dependsOn: [redisBuildingCreateTest] +} +function redisBuildingReadOneTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Building buildingRetrieved = check rainierClient->/buildings/[building1.buildingCode]; + test:assertEquals(buildingRetrieved, building1); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"], + dependsOn: [redisBuildingCreateTest] +} +function redisBuildingReadOneTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Building|error buildingRetrieved = rainierClient->/buildings/["invalid-building-code"]; + if buildingRetrieved is persist:NotFoundError { + test:assertEquals(buildingRetrieved.message(), + "A record with the key 'Building:invalid-building-code' does not exist for the entity 'Building'."); + } else { + test:assertFail("persist:NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"], + dependsOn: [redisBuildingCreateTest, redisBuildingCreateTest2] +} +function redisBuildingReadManyTest() returns error? { + RedisRainierClient rainierClient = check new (); + + stream buildingStream = rainierClient->/buildings; + Building[] buildings = check from Building building in buildingStream + order by building.buildingCode ascending select building; + + test:assertEquals(buildings, [building1, building2, building3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis", "dependent"], + dependsOn: [redisBuildingCreateTest, redisBuildingCreateTest2] +} +function redisBuildingReadManyDependentTest() returns error? { + RedisRainierClient rainierClient = check new (); + + stream buildingStream = rainierClient->/buildings; + BuildingInfo2[] buildings = check from BuildingInfo2 building in buildingStream + order by building.postalCode ascending select building; + + test:assertEquals(buildings, [ + {city: building1.city, state: building1.state, country: building1.country, postalCode: building1.postalCode, + 'type: building1.'type}, + {city: building2.city, state: building2.state, country: building2.country, postalCode: building2.postalCode, + 'type: building2.'type}, + {city: building3.city, state: building3.state, country: building3.country, postalCode: building3.postalCode, + 'type: building3.'type} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"], + dependsOn: [redisBuildingReadOneTest, redisBuildingReadManyTest, redisBuildingReadManyDependentTest] +} +function redisBuildingUpdateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Building building = check rainierClient->/buildings/[building1.buildingCode].put({ + city: "Galle", + state: "Southern Province", + postalCode: "10890", + 'type: "owned" + }); + + test:assertEquals(building, updatedBuilding1); + + Building buildingRetrieved = check rainierClient->/buildings/[building1.buildingCode]; + test:assertEquals(buildingRetrieved, updatedBuilding1); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"], + dependsOn: [redisBuildingReadOneTest, redisBuildingReadManyTest, redisBuildingReadManyDependentTest] +} +function redisBuildingUpdateTestNegative1() returns error? { + RedisRainierClient rainierClient = check new (); + + Building|error building = rainierClient->/buildings/["invalid-building-code"].put({ + city: "Galle", + state: "Southern Province", + postalCode: "10890" + }); + + if building is persist:NotFoundError { + test:assertEquals(building.message(), + "A record with the key 'Building:invalid-building-code' does not exist for the entity 'Building'."); + } else { + test:assertFail("persist:NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"], + dependsOn: [redisBuildingUpdateTest] +} +function redisBuildingDeleteTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Building building = check rainierClient->/buildings/[building1.buildingCode].delete(); + test:assertEquals(building, updatedBuilding1); + + stream buildingStream = rainierClient->/buildings; + Building[] buildings = check from Building building2 in buildingStream + order by building2.buildingCode ascending select building2; + + test:assertEquals(buildings, [building2, building3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "redis"], + dependsOn: [redisBuildingDeleteTest] +} +function redisBuildingDeleteTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Building|error building = rainierClient->/buildings/[building1.buildingCode].delete(); + + if building is error { + test:assertEquals(building.message(), + string `A record with the key 'Building:${building1.buildingCode}' does not exist for the entity 'Building'.`); + } else { + test:assertFail("persist:NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/redis-composite-key-tests.bal b/ballerina/tests/redis-composite-key-tests.bal new file mode 100644 index 0000000..ad181ba --- /dev/null +++ b/ballerina/tests/redis-composite-key-tests.bal @@ -0,0 +1,203 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; +import ballerina/test; + +@test:Config { + groups: ["composite-key", "redis"] +} +function redisCompositeKeyCreateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + [string, string][] ids = check rainierClient->/orderitems.post([orderItem1, orderItem2]); + test:assertEquals(ids, [[orderItem1.orderId, orderItem1.itemId], [orderItem2.orderId, orderItem2.itemId]]); + + OrderItem orderItemRetrieved = check rainierClient->/orderitems/[orderItem1.orderId]/[orderItem1.itemId]; + test:assertEquals(orderItemRetrieved, orderItem1); + + orderItemRetrieved = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId]; + test:assertEquals(orderItemRetrieved, orderItem2); + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyCreateTest] +} +function redisCompositeKeyCreateTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + [string, string][]|error ids = rainierClient->/orderitems.post([orderItem1]); + if ids is persist:AlreadyExistsError { + test:assertEquals(ids.message(), + "Record(s) already exist with the same key for the entity 'OrderItem'. Number of keys exists : 1"); + } else { + test:assertFail("persist:AlreadyExistsError expected"); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyCreateTest] +} +function redisCompositeKeyReadManyTest() returns error? { + RedisRainierClient rainierClient = check new (); + + stream orderItemStream = rainierClient->/orderitems; + OrderItem[] orderitem = check from OrderItem orderItem in orderItemStream + order by orderItem.orderId ascending select orderItem; + + test:assertEquals(orderitem, [orderItem1, orderItem2]); + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyCreateTest] +} +function redisCompositeKeyReadOneTest() returns error? { + RedisRainierClient rainierClient = check new (); + OrderItem orderItem = check rainierClient->/orderitems/[orderItem1.orderId]/[orderItem1.itemId]; + test:assertEquals(orderItem, orderItem1); + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key2"], + dependsOn: [redisCompositeKeyCreateTest] +} +function redisCompositeKeyReadOneTest2() returns error? { + RedisRainierClient rainierClient = check new (); + OrderItem orderItem = check rainierClient->/orderitems/[orderItem1.orderId]/[orderItem1.itemId]; + test:assertEquals(orderItem, orderItem1); + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyCreateTest] +} +function redisCompositeKeyReadOneTestNegative1() returns error? { + RedisRainierClient rainierClient = check new (); + OrderItem|error orderItem = rainierClient->/orderitems/["invalid-order-id"]/[orderItem1.itemId]; + + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), + "A record with the key 'OrderItem:invalid-order-id:item-1' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyCreateTest] +} +function redisCompositeKeyReadOneTestNegative2() returns error? { + RedisRainierClient rainierClient = check new (); + OrderItem|error orderItem = rainierClient->/orderitems/[orderItem1.orderId]/["invalid-item-id"]; + + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), + "A record with the key 'OrderItem:order-1:invalid-item-id' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyCreateTest, redisCompositeKeyReadOneTest, redisCompositeKeyReadManyTest, + redisCompositeKeyReadOneTest2] +} +function redisCompositeKeyUpdateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + OrderItem orderItem = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId].put({ + quantity: orderItem2Updated.quantity, + notes: orderItem2Updated.notes + }); + test:assertEquals(orderItem, orderItem2Updated); + + orderItem = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId]; + test:assertEquals(orderItem, orderItem2Updated); + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyCreateTest, redisCompositeKeyReadOneTest, redisCompositeKeyReadManyTest, + redisCompositeKeyReadOneTest2] +} +function redisCompositeKeyUpdateTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + OrderItem|error orderItem = rainierClient->/orderitems/[orderItem1.orderId]/[orderItem2.itemId].put({ + quantity: 239, + notes: "updated notes" + }); + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), + "A record with the key 'OrderItem:order-1:item-2' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyUpdateTest] +} +function redisCompositeKeyDeleteTest() returns error? { + RedisRainierClient rainierClient = check new (); + + OrderItem orderItem = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId].delete(); + test:assertEquals(orderItem, orderItem2Updated); + + OrderItem|error orderItemRetrieved = rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId]; + test:assertTrue(orderItemRetrieved is persist:NotFoundError); + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "redis"], + dependsOn: [redisCompositeKeyUpdateTest] +} +function redisCompositeKeyDeleteTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + OrderItem|error orderItem = rainierClient->/orderitems/["invalid-order-id"]/[orderItem2.itemId].delete(); + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), + "A record with the key 'OrderItem:invalid-order-id:item-2' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} diff --git a/ballerina/tests/redis-department-tests.bal b/ballerina/tests/redis-department-tests.bal new file mode 100644 index 0000000..86bdb3c --- /dev/null +++ b/ballerina/tests/redis-department-tests.bal @@ -0,0 +1,186 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; +import ballerina/test; + +@test:Config { + groups: ["department", "redis"] +} +function redisDepartmentCreateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] deptNos = check rainierClient->/departments.post([department1]); + test:assertEquals(deptNos, [department1.deptNo]); + + Department departmentRetrieved = check rainierClient->/departments/[department1.deptNo]; + test:assertEquals(departmentRetrieved, department1); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"] +} +function redisDepartmentCreateTest2() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] deptNos = check rainierClient->/departments.post([department2, department3]); + + test:assertEquals(deptNos, [department2.deptNo, department3.deptNo]); + + Department departmentRetrieved = check rainierClient->/departments/[department2.deptNo]; + test:assertEquals(departmentRetrieved, department2); + + departmentRetrieved = check rainierClient->/departments/[department3.deptNo]; + test:assertEquals(departmentRetrieved, department3); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"], + dependsOn: [redisDepartmentCreateTest] +} +function redisDepartmentReadOneTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Department departmentRetrieved = check rainierClient->/departments/[department1.deptNo]; + test:assertEquals(departmentRetrieved, department1); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"], + dependsOn: [redisDepartmentCreateTest] +} +function redisDepartmentReadOneTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Department|error departmentRetrieved = rainierClient->/departments/["invalid-department-id"]; + if departmentRetrieved is persist:NotFoundError { + test:assertEquals(departmentRetrieved.message(), + "A record with the key 'Department:invalid-department-id' does not exist for the entity 'Department'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"], + dependsOn: [redisDepartmentCreateTest, redisDepartmentCreateTest2] +} +function redisDepartmentReadManyTest() returns error? { + RedisRainierClient rainierClient = check new (); + stream departmentStream = rainierClient->/departments; + Department[] departments = check from Department department in departmentStream + order by department.deptNo ascending select department; + + test:assertEquals(departments, [department1, department2, department3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis", "dependent"], + dependsOn: [redisDepartmentCreateTest, redisDepartmentCreateTest2] +} +function redisDepartmentReadManyTestDependent() returns error? { + RedisRainierClient rainierClient = check new (); + + stream departmentStream = rainierClient->/departments; + DepartmentInfo2[] departments = check from DepartmentInfo2 department in departmentStream + order by department.deptName ascending select department; + + test:assertEquals(departments, [ + {deptName: department3.deptName}, + {deptName: department1.deptName}, + {deptName: department2.deptName} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"], + dependsOn: [redisDepartmentReadOneTest, redisDepartmentReadManyTest, redisDepartmentReadManyTestDependent] +} +function redisDepartmentUpdateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Department department = check rainierClient->/departments/[department1.deptNo].put({ + deptName: "Finance & Legalities" + }); + + test:assertEquals(department, updatedDepartment1); + + Department departmentRetrieved = check rainierClient->/departments/[department1.deptNo]; + test:assertEquals(departmentRetrieved, updatedDepartment1); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"], + dependsOn: [redisDepartmentReadOneTest, redisDepartmentReadManyTest, redisDepartmentReadManyTestDependent] +} +function redisDepartmentUpdateTestNegative1() returns error? { + RedisRainierClient rainierClient = check new (); + + Department|error department = rainierClient->/departments/["invalid-department-id"].put({ + deptName: "Human Resources" + }); + + if department is persist:NotFoundError { + test:assertEquals(department.message(), + "A record with the key 'Department:invalid-department-id' does not exist for the entity 'Department'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"], + dependsOn: [redisDepartmentUpdateTest] +} +function redisDepartmentDeleteTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Department department = check rainierClient->/departments/[department1.deptNo].delete(); + test:assertEquals(department, updatedDepartment1); + + stream departmentStream = rainierClient->/departments; + Department[] departments = check from Department department2 in departmentStream + order by department2.deptNo ascending select department2; + + test:assertEquals(departments, [department2, department3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "redis"], + dependsOn: [redisDepartmentDeleteTest] +} +function redisDepartmentDeleteTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Department|error department = rainierClient->/departments/[department1.deptNo].delete(); + + if department is persist:NotFoundError { + test:assertEquals(department.message(), + string `A record with the key 'Department:${department1.deptNo}' does not exist for the entity 'Department'.`); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/redis-employee-tests.bal b/ballerina/tests/redis-employee-tests.bal new file mode 100644 index 0000000..b4564ef --- /dev/null +++ b/ballerina/tests/redis-employee-tests.bal @@ -0,0 +1,236 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; +import ballerina/test; + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisWorkspaceDeleteTestNegative, redisDepartmentDeleteTestNegative] +} +function redisEmployeeCreateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] empNos = check rainierClient->/employees.post([employee1]); + test:assertEquals(empNos, [employee1.empNo]); + + Employee employeeRetrieved = check rainierClient->/employees/[employee1.empNo]; + test:assertEquals(employeeRetrieved, employee1); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisWorkspaceDeleteTestNegative, redisDepartmentDeleteTestNegative] +} +function redisEmployeeCreateTest2() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] empNos = check rainierClient->/employees.post([employee2, employee3]); + + test:assertEquals(empNos, [employee2.empNo, employee3.empNo]); + + Employee employeeRetrieved = check rainierClient->/employees/[employee2.empNo]; + test:assertEquals(employeeRetrieved, employee2); + + employeeRetrieved = check rainierClient->/employees/[employee3.empNo]; + test:assertEquals(employeeRetrieved, employee3); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeCreateTest] +} +function redisEmployeeReadOneTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee employeeRetrieved = check rainierClient->/employees/[employee1.empNo]; + test:assertEquals(employeeRetrieved, employee1); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeCreateTest] +} +function redisEmployeeReadOneTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee|error employeeRetrieved = rainierClient->/employees/["invalid-employee-id"]; + if employeeRetrieved is persist:NotFoundError { + test:assertEquals(employeeRetrieved.message(), + "A record with the key 'Employee:invalid-employee-id' does not exist for the entity 'Employee'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeCreateTest, redisEmployeeCreateTest2] +} +function redisEmployeeReadManyTest() returns error? { + RedisRainierClient rainierClient = check new (); + + stream employeeStream = rainierClient->/employees; + Employee[] employees = check from Employee employee in employeeStream + order by employee.empNo ascending select employee; + + test:assertEquals(employees, [employee1, employee2, employee3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["dependent", "employee"], + dependsOn: [redisEmployeeCreateTest, redisEmployeeCreateTest2] +} +function redisEmployeeReadManyDependentTest1() returns error? { + RedisRainierClient rainierClient = check new (); + + stream employeeStream = rainierClient->/employees; + EmployeeName[] employees = check from EmployeeName employee in employeeStream + order by employee.firstName ascending select employee; + + test:assertEquals(employees, [ + {firstName: employee3.firstName, lastName: employee3.lastName}, + {firstName: employee2.firstName, lastName: employee2.lastName}, + {firstName: employee1.firstName, lastName: employee1.lastName} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["dependent", "employee"], + dependsOn: [redisEmployeeCreateTest, redisEmployeeCreateTest2] +} +function redisEmployeeReadManyDependentTest2() returns error? { + RedisRainierClient rainierClient = check new (); + + stream employeeStream = rainierClient->/employees; + EmployeeInfo2[] employees = check from EmployeeInfo2 employee in employeeStream + order by employee.empNo ascending select employee; + + test:assertEquals(employees, [ + {empNo: employee1.empNo, birthDate: employee1.birthDate, departmentDeptNo: employee1.departmentDeptNo, + workspaceWorkspaceId: employee1.workspaceWorkspaceId}, + {empNo: employee2.empNo, birthDate: employee2.birthDate, departmentDeptNo: employee2.departmentDeptNo, + workspaceWorkspaceId: employee2.workspaceWorkspaceId}, + {empNo: employee3.empNo, birthDate: employee3.birthDate, departmentDeptNo: employee3.departmentDeptNo, + workspaceWorkspaceId: employee3.workspaceWorkspaceId} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeReadOneTest, redisEmployeeReadManyTest, redisEmployeeReadManyDependentTest1, + redisEmployeeReadManyDependentTest2] +} +function redisEmployeeUpdateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee employee = check rainierClient->/employees/[employee1.empNo].put({ + lastName: updatedEmployee1.lastName, + departmentDeptNo: updatedEmployee1.departmentDeptNo, + birthDate: updatedEmployee1.birthDate + }); + + test:assertEquals(employee, updatedEmployee1); + + Employee employeeRetrieved = check rainierClient->/employees/[employee1.empNo]; + test:assertEquals(employeeRetrieved, updatedEmployee1); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeReadOneTest, redisEmployeeReadManyTest, redisEmployeeReadManyDependentTest1, + redisEmployeeReadManyDependentTest2] +} +function redisEmployeeUpdateTestNegative1() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee|error employee = rainierClient->/employees/["invalid-employee-id"].put({ + lastName: "Jones" + }); + + if employee is persist:NotFoundError { + test:assertEquals(employee.message(), + "A record with the key 'Employee:invalid-employee-id' does not exist for the entity 'Employee'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeReadOneTest, redisEmployeeReadManyTest, redisEmployeeReadManyDependentTest1, + redisEmployeeReadManyDependentTest2] +} +function redisEmployeeUpdateTestNegative3() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee|error employee = rainierClient->/employees/[employee1.empNo].put({ + workspaceWorkspaceId: "invalid-workspaceWorkspaceId" + }); + + if employee is persist:ConstraintViolationError { + test:assertTrue(employee.message().includes( + string `An association constraint failed between entities 'Employee' and 'Workspace'`)); + } else { + test:assertFail("persist:ConstraintViolationError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeUpdateTest, redisEmployeeUpdateTestNegative3] +} +function redisEmployeeDeleteTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee employee = check rainierClient->/employees/[employee1.empNo].delete(); + test:assertEquals(employee, updatedEmployee1); + + stream employeeStream = rainierClient->/employees; + Employee[] employees = check from Employee employee2 in employeeStream + order by employee2.empNo ascending select employee2; + + test:assertEquals(employees, [employee2, employee3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "redis"], + dependsOn: [redisEmployeeDeleteTest] +} +function redisEmployeeDeleteTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Employee|error employee = rainierClient->/employees/[employee1.empNo].delete(); + + if employee is persist:NotFoundError { + test:assertEquals(employee.message(), + string `A record with the key 'Employee:${employee1.empNo}' does not exist for the entity 'Employee'.`); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/redis-id-fields-tests.bal b/ballerina/tests/redis-id-fields-tests.bal new file mode 100644 index 0000000..ade16bc --- /dev/null +++ b/ballerina/tests/redis-id-fields-tests.bal @@ -0,0 +1,539 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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; + +@test:Config { + groups: ["id-fields", "redis"] +} +function redisIntIdFieldTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + IntIdRecord intIdRecord1 = { + id: 1, + randomField: "test1" + }; + IntIdRecord intIdRecord2 = { + id: 2, + randomField: "test2" + }; + IntIdRecord intIdRecord3 = { + id: 3, + randomField: "test3" + }; + IntIdRecord intIdRecord1Updated = { + id: 1, + randomField: "test1Updated" + }; + + // create + int[] ids = check testEntitiesClient->/intidrecords.post([intIdRecord1, intIdRecord2, intIdRecord3]); + test:assertEquals(ids, [intIdRecord1.id, intIdRecord2.id, intIdRecord3.id]); + + // read one + IntIdRecord retrievedRecord1 = check testEntitiesClient->/intidrecords/[intIdRecord1.id]; + test:assertEquals(intIdRecord1, retrievedRecord1); + + // read one dependent + IntIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/intidrecords/[intIdRecord1.id]; + test:assertEquals({randomField: intIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + IntIdRecord[] intIdRecords = check from IntIdRecord intIdRecord in + testEntitiesClient->/intidrecords.get(IntIdRecord) + order by intIdRecord.id ascending select intIdRecord; + test:assertEquals(intIdRecords, [intIdRecord1, intIdRecord2, intIdRecord3]); + + // read dependent + IntIdRecordDependent[] intIdRecordsDependent = check from IntIdRecordDependent intIdRecord in + testEntitiesClient->/intidrecords.get(IntIdRecordDependent) + order by intIdRecord.randomField ascending select intIdRecord; + test:assertEquals(intIdRecordsDependent, [{randomField: intIdRecord1.randomField}, + {randomField: intIdRecord2.randomField}, {randomField: intIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/intidrecords/ + [intIdRecord1.id].put({randomField: intIdRecord1Updated.randomField}); + test:assertEquals(intIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/intidrecords/[intIdRecord1.id]; + test:assertEquals(intIdRecord1Updated, retrievedRecord1); + + // delete + IntIdRecord retrievedRecord2 = check testEntitiesClient->/intidrecords/[intIdRecord2.id].delete(); + test:assertEquals(intIdRecord2, retrievedRecord2); + intIdRecords = check from IntIdRecord intIdRecord in testEntitiesClient->/intidrecords.get(IntIdRecord) + order by intIdRecord.id ascending select intIdRecord; + test:assertEquals(intIdRecords, [intIdRecord1Updated, intIdRecord3]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "redis"] +} +function redisStringIdFieldTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + StringIdRecord stringIdRecord1 = { + id: "id-1", + randomField: "test1" + }; + StringIdRecord stringIdRecord2 = { + id: "id-2", + randomField: "test2" + }; + StringIdRecord stringIdRecord3 = { + id: "id-3", + randomField: "test3" + }; + StringIdRecord stringIdRecord1Updated = { + id: "id-1", + randomField: "test1Updated" + }; + + // create + string[] ids = check testEntitiesClient->/stringidrecords.post([stringIdRecord1, stringIdRecord2, stringIdRecord3]); + test:assertEquals(ids, [stringIdRecord1.id, stringIdRecord2.id, stringIdRecord3.id]); + + // read one + StringIdRecord retrievedRecord1 = check testEntitiesClient->/stringidrecords/[stringIdRecord1.id]; + test:assertEquals(stringIdRecord1, retrievedRecord1); + + // read one dependent + StringIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/stringidrecords/[stringIdRecord1.id]; + test:assertEquals({randomField: stringIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + StringIdRecord[] stringIdRecords = check from StringIdRecord stringIdRecord in + testEntitiesClient->/stringidrecords.get(StringIdRecord) + order by stringIdRecord.id ascending select stringIdRecord; + test:assertEquals(stringIdRecords, [stringIdRecord1, stringIdRecord2, stringIdRecord3]); + + // read dependent + StringIdRecordDependent[] stringIdRecordsDependent = check from StringIdRecordDependent stringIdRecord in + testEntitiesClient->/stringidrecords.get(StringIdRecordDependent) + order by stringIdRecord.randomField ascending select stringIdRecord; + test:assertEquals(stringIdRecordsDependent, [{randomField: stringIdRecord1.randomField}, + {randomField: stringIdRecord2.randomField}, {randomField: stringIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/stringidrecords/ + [stringIdRecord1.id].put({randomField: stringIdRecord1Updated.randomField}); + test:assertEquals(stringIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/stringidrecords/[stringIdRecord1.id]; + test:assertEquals(stringIdRecord1Updated, retrievedRecord1); + + // delete + StringIdRecord retrievedRecord2 = check testEntitiesClient->/stringidrecords/[stringIdRecord2.id].delete(); + test:assertEquals(stringIdRecord2, retrievedRecord2); + stringIdRecords = check from StringIdRecord stringIdRecord in + testEntitiesClient->/stringidrecords.get(StringIdRecord) + order by stringIdRecord.id ascending select stringIdRecord; + test:assertEquals(stringIdRecords, [stringIdRecord1Updated, stringIdRecord3]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "redis"] +} +function redisFloatIdFieldTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + FloatIdRecord floatIdRecord1 = { + id: 1.0, + randomField: "test1" + }; + FloatIdRecord floatIdRecord2 = { + id: 2.0, + randomField: "test2" + }; + FloatIdRecord floatIdRecord3 = { + id: 3.0, + randomField: "test3" + }; + FloatIdRecord floatIdRecord1Updated = { + id: 1.0, + randomField: "test1Updated" + }; + + // create + float[] ids = check testEntitiesClient->/floatidrecords.post([floatIdRecord1, floatIdRecord2, floatIdRecord3]); + test:assertEquals(ids, [floatIdRecord1.id, floatIdRecord2.id, floatIdRecord3.id]); + + // read one + FloatIdRecord retrievedRecord1 = check testEntitiesClient->/floatidrecords/[floatIdRecord1.id]; + test:assertEquals(floatIdRecord1, retrievedRecord1); + + // read one dependent + FloatIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/floatidrecords/[floatIdRecord1.id]; + test:assertEquals({randomField: floatIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + FloatIdRecord[] floatIdRecords = check from FloatIdRecord floatIdRecord in + testEntitiesClient->/floatidrecords.get(FloatIdRecord) + order by floatIdRecord.id ascending select floatIdRecord; + test:assertEquals(floatIdRecords, [floatIdRecord1, floatIdRecord2, floatIdRecord3]); + + // read dependent + FloatIdRecordDependent[] floatIdRecordsDependent = check from FloatIdRecordDependent floatIdRecord in + testEntitiesClient->/floatidrecords.get(FloatIdRecordDependent) + order by floatIdRecord.randomField ascending select floatIdRecord; + test:assertEquals(floatIdRecordsDependent, [{randomField: floatIdRecord1.randomField}, + {randomField: floatIdRecord2.randomField}, {randomField: floatIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/floatidrecords/ + [floatIdRecord1.id].put({randomField: floatIdRecord1Updated.randomField}); + test:assertEquals(floatIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/floatidrecords/[floatIdRecord1.id]; + test:assertEquals(floatIdRecord1Updated, retrievedRecord1); + + // delete + FloatIdRecord retrievedRecord2 = check testEntitiesClient->/floatidrecords/[floatIdRecord2.id].delete(); + test:assertEquals(floatIdRecord2, retrievedRecord2); + floatIdRecords = check from FloatIdRecord floatIdRecord in testEntitiesClient->/floatidrecords.get(FloatIdRecord) + order by floatIdRecord.id ascending select floatIdRecord; + test:assertEquals(floatIdRecords, [floatIdRecord1Updated, floatIdRecord3]); +} + +@test:Config { + groups: ["id-fields", "redis"] +} +function redisDecimalIdFieldTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + DecimalIdRecord decimalIdRecord1 = { + id: 1.1d, + randomField: "test1" + }; + DecimalIdRecord decimalIdRecord2 = { + id: 2.2d, + randomField: "test2" + }; + DecimalIdRecord decimalIdRecord3 = { + id: 3.3d, + randomField: "test3" + }; + DecimalIdRecord decimalIdRecord1Updated = { + id: 1.1d, + randomField: "test1Updated" + }; + + // create + decimal[] ids = check testEntitiesClient->/decimalidrecords.post([decimalIdRecord1, decimalIdRecord2, + decimalIdRecord3]); + test:assertEquals(ids, [decimalIdRecord1.id, decimalIdRecord2.id, decimalIdRecord3.id]); + + // read one + DecimalIdRecord retrievedRecord1 = check testEntitiesClient->/decimalidrecords/[decimalIdRecord1.id]; + test:assertEquals(decimalIdRecord1, retrievedRecord1); + + // read one dependent + DecimalIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/decimalidrecords/ + [decimalIdRecord1.id]; + test:assertEquals({randomField: decimalIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + DecimalIdRecord[] decimalIdRecords = check from DecimalIdRecord decimalIdRecord in testEntitiesClient->/ + decimalidrecords.get(DecimalIdRecord) + order by decimalIdRecord.id ascending select decimalIdRecord; + test:assertEquals(decimalIdRecords, [decimalIdRecord1, decimalIdRecord2, decimalIdRecord3]); + + // read dependent + DecimalIdRecordDependent[] decimalIdRecordsDependent = check from DecimalIdRecordDependent decimalIdRecord in + testEntitiesClient->/decimalidrecords.get(DecimalIdRecordDependent) + order by decimalIdRecord.randomField ascending select decimalIdRecord; + test:assertEquals(decimalIdRecordsDependent, [{randomField: decimalIdRecord1.randomField}, + {randomField: decimalIdRecord2.randomField}, {randomField: decimalIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/decimalidrecords/ + [decimalIdRecord1.id].put({randomField: decimalIdRecord1Updated.randomField}); + test:assertEquals(decimalIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/decimalidrecords/[decimalIdRecord1.id]; + test:assertEquals(decimalIdRecord1Updated, retrievedRecord1); + + // delete + DecimalIdRecord retrievedRecord2 = check testEntitiesClient->/decimalidrecords/[decimalIdRecord2.id].delete(); + test:assertEquals(decimalIdRecord2, retrievedRecord2); + decimalIdRecords = check from DecimalIdRecord decimalIdRecord in + testEntitiesClient->/decimalidrecords.get(DecimalIdRecord) + order by decimalIdRecord.id ascending select decimalIdRecord; + test:assertEquals(decimalIdRecords, [decimalIdRecord1Updated, decimalIdRecord3]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "redis"], + enable: false +} +function redisBooleanIdFieldTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + BooleanIdRecord booleanIdRecord1 = { + id: true, + randomField: "test1" + }; + BooleanIdRecord booleanIdRecord2 = { + id: false, + randomField: "test2" + }; + BooleanIdRecord booleanIdRecord1Updated = { + id: true, + randomField: "test1Updated" + }; + + // create + boolean[] ids = check testEntitiesClient->/booleanidrecords.post([booleanIdRecord1, booleanIdRecord2]); + test:assertEquals(ids, [booleanIdRecord1.id, booleanIdRecord2.id]); + + // read one + BooleanIdRecord retrievedRecord1 = check testEntitiesClient->/booleanidrecords/[booleanIdRecord1.id]; + test:assertEquals(booleanIdRecord1, retrievedRecord1); + + // read one dependent + BooleanIdRecordDependent retrievedRecord1Dependent = + check testEntitiesClient->/booleanidrecords/[booleanIdRecord1.id]; + test:assertEquals({randomField: booleanIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + BooleanIdRecord[] booleanIdRecords = check from BooleanIdRecord booleanIdRecord in + testEntitiesClient->/booleanidrecords.get(BooleanIdRecord) + order by booleanIdRecord.randomField ascending select booleanIdRecord; + test:assertEquals(booleanIdRecords, [booleanIdRecord1, booleanIdRecord2]); + + // read dependent + BooleanIdRecordDependent[] booleanIdRecordsDependent = check from BooleanIdRecordDependent booleanIdRecord in + testEntitiesClient->/booleanidrecords.get(BooleanIdRecordDependent) + select booleanIdRecord; + test:assertEquals(booleanIdRecordsDependent, [{randomField: booleanIdRecord2.randomField}, + {randomField: booleanIdRecord1.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/booleanidrecords/ + [booleanIdRecord1.id].put({randomField: booleanIdRecord1Updated.randomField}); + test:assertEquals(booleanIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/booleanidrecords/[booleanIdRecord1.id]; + test:assertEquals(booleanIdRecord1Updated, retrievedRecord1); + + // delete + BooleanIdRecord retrievedRecord2 = check testEntitiesClient->/booleanidrecords/[booleanIdRecord2.id].delete(); + test:assertEquals(booleanIdRecord2, retrievedRecord2); + booleanIdRecords = check from BooleanIdRecord booleanIdRecord in + testEntitiesClient->/booleanidrecords.get(BooleanIdRecord) + select booleanIdRecord; + test:assertEquals(booleanIdRecords, [booleanIdRecord1Updated]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "redis"], + enable: false +} +function redisAllTypesIdFieldTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + AllTypesIdRecord allTypesIdRecord1 = { + intType: 1, + stringType: "id-1", + floatType: 1.0, + booleanType: true, + decimalType: 1.1d, + randomField: "test1" + }; + AllTypesIdRecord allTypesIdRecord2 = { + intType: 2, + stringType: "id-2", + floatType: 2.0, + booleanType: false, + decimalType: 2.2d, + randomField: "test2" + }; + AllTypesIdRecord allTypesIdRecord1Updated = { + intType: 1, + stringType: "id-1", + floatType: 1.0, + booleanType: true, + decimalType: 1.1d, + randomField: "test1Updated" + }; + + // create + [boolean, int, float, decimal, string][] ids = + check testEntitiesClient->/alltypesidrecords.post([allTypesIdRecord1, allTypesIdRecord2]); + test:assertEquals(ids, [ + [allTypesIdRecord1.booleanType, allTypesIdRecord1.intType, allTypesIdRecord1.floatType, + allTypesIdRecord1.decimalType, allTypesIdRecord1.stringType], + [allTypesIdRecord2.booleanType, allTypesIdRecord2.intType, allTypesIdRecord2.floatType, + allTypesIdRecord2.decimalType, allTypesIdRecord2.stringType] + ]); + + // read one + AllTypesIdRecord retrievedRecord1 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord1.booleanType]/ + [allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/[allTypesIdRecord1.decimalType]/ + [allTypesIdRecord1.stringType]; + test:assertEquals(allTypesIdRecord1, retrievedRecord1); + + // read one dependent + AllTypesIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/alltypesidrecords/ + [allTypesIdRecord1.booleanType]/[allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/ + [allTypesIdRecord1.decimalType]/[allTypesIdRecord1.stringType]; + test:assertEquals({randomField: allTypesIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + AllTypesIdRecord[] allTypesIdRecords = check from AllTypesIdRecord allTypesIdRecord in + testEntitiesClient->/alltypesidrecords.get(AllTypesIdRecord) + select allTypesIdRecord; + test:assertEquals(allTypesIdRecords, [allTypesIdRecord2, allTypesIdRecord1]); + + // read dependent + AllTypesIdRecordDependent[] allTypesIdRecordsDependent = check from AllTypesIdRecordDependent allTypesIdRecord in + testEntitiesClient->/alltypesidrecords.get(AllTypesIdRecordDependent) + select allTypesIdRecord; + test:assertEquals(allTypesIdRecordsDependent, [{randomField: allTypesIdRecord2.randomField}, + {randomField: allTypesIdRecord1.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord1.booleanType]/ + [allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/[allTypesIdRecord1.decimalType]/ + [allTypesIdRecord1.stringType].put({randomField: allTypesIdRecord1Updated.randomField}); + test:assertEquals(allTypesIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord1.booleanType]/ + [allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/[allTypesIdRecord1.decimalType]/ + [allTypesIdRecord1.stringType]; + test:assertEquals(allTypesIdRecord1Updated, retrievedRecord1); + + // delete + AllTypesIdRecord retrievedRecord2 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord2.booleanType]/ + [allTypesIdRecord2.intType]/[allTypesIdRecord2.floatType]/[allTypesIdRecord2.decimalType]/ + [allTypesIdRecord2.stringType].delete(); + test:assertEquals(allTypesIdRecord2, retrievedRecord2); + allTypesIdRecords = check from AllTypesIdRecord allTypesIdRecord in + testEntitiesClient->/alltypesidrecords.get(AllTypesIdRecord) + select allTypesIdRecord; + test:assertEquals(allTypesIdRecords, [allTypesIdRecord1Updated]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "redis", "associations"], + dependsOn: [redisAllTypesIdFieldTest], + enable: false +} +function redisCompositeAssociationsTest() returns error? { + RedisTestEntitiesClient testEntitiesClient = check new (); + + CompositeAssociationRecord compositeAssociationRecord1 = { + id: "id-1", + randomField: "test1", + alltypesidrecordIntType: 1, + alltypesidrecordStringType: "id-1", + alltypesidrecordFloatType: 1.0, + alltypesidrecordBooleanType: true, + alltypesidrecordDecimalType: 1.10 + }; + + CompositeAssociationRecord compositeAssociationRecord2 = { + id: "id-2", + randomField: "test2", + alltypesidrecordIntType: 1, + alltypesidrecordStringType: "id-1", + alltypesidrecordFloatType: 1.0, + alltypesidrecordBooleanType: true, + alltypesidrecordDecimalType: 1.10 + }; + + CompositeAssociationRecord compositeAssociationRecordUpdated1 = { + id: "id-1", + randomField: "test1Updated", + alltypesidrecordIntType: 1, + alltypesidrecordStringType: "id-1", + alltypesidrecordFloatType: 1.0, + alltypesidrecordBooleanType: true, + alltypesidrecordDecimalType: 1.10 + }; + + AllTypesIdRecordOptionalized allTypesIdRecord1 = { + intType: 1, + stringType: "id-1", + floatType: 1.0, + booleanType: true, + decimalType: 1.10, + randomField: "test1Updated" + }; + + // create + string[] ids = check testEntitiesClient->/compositeassociationrecords.post([compositeAssociationRecord1, + compositeAssociationRecord2]); + test:assertEquals(ids, [compositeAssociationRecord1.id, compositeAssociationRecord2.id]); + + // read one + CompositeAssociationRecord retrievedRecord1 = + check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord1.id]; + test:assertEquals(compositeAssociationRecord1, retrievedRecord1); + + // read one dependent + CompositeAssociationRecordDependent retrievedRecord1Dependent = + check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord1.id]; + test:assertEquals({ + randomField: compositeAssociationRecord1.randomField, + alltypesidrecordIntType: compositeAssociationRecord1.alltypesidrecordIntType, + alltypesidrecordDecimalType: compositeAssociationRecord1.alltypesidrecordDecimalType, + allTypesIdRecord: {intType: allTypesIdRecord1.intType, stringType: allTypesIdRecord1.stringType, + booleanType: allTypesIdRecord1.booleanType, randomField: allTypesIdRecord1.randomField} + }, retrievedRecord1Dependent); + + // read + CompositeAssociationRecord[] compositeAssociationRecords = check from + CompositeAssociationRecord compositeAssociationRecord in + testEntitiesClient->/compositeassociationrecords.get(CompositeAssociationRecord) + order by compositeAssociationRecord.id ascending select compositeAssociationRecord; + test:assertEquals(compositeAssociationRecords, [compositeAssociationRecord1, compositeAssociationRecord2]); + + // read dependent + CompositeAssociationRecordDependent[] compositeAssociationRecordsDependent = + check from CompositeAssociationRecordDependent compositeAssociationRecord in + testEntitiesClient->/compositeassociationrecords.get(CompositeAssociationRecordDependent) + order by compositeAssociationRecord.randomField ascending select compositeAssociationRecord; + test:assertEquals(compositeAssociationRecordsDependent, [ + {randomField: compositeAssociationRecord1.randomField, + alltypesidrecordIntType: compositeAssociationRecord1.alltypesidrecordIntType, + alltypesidrecordDecimalType: compositeAssociationRecord1.alltypesidrecordDecimalType, + allTypesIdRecord: {intType: allTypesIdRecord1.intType, stringType: allTypesIdRecord1.stringType, + booleanType: allTypesIdRecord1.booleanType, randomField: allTypesIdRecord1.randomField}}, + {randomField: compositeAssociationRecord2.randomField, + alltypesidrecordIntType: compositeAssociationRecord2.alltypesidrecordIntType, + alltypesidrecordDecimalType: compositeAssociationRecord2.alltypesidrecordDecimalType, + allTypesIdRecord: {intType: allTypesIdRecord1.intType, stringType: allTypesIdRecord1.stringType, + booleanType: allTypesIdRecord1.booleanType, randomField: allTypesIdRecord1.randomField}} + ]); + + // update + retrievedRecord1 = check testEntitiesClient->/compositeassociationrecords + /[compositeAssociationRecord1.id].put({randomField: "test1Updated"}); + test:assertEquals(compositeAssociationRecordUpdated1, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord1.id]; + test:assertEquals(compositeAssociationRecordUpdated1, retrievedRecord1); + + // delete + CompositeAssociationRecord retrievedRecord2 = + check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord2.id].delete(); + test:assertEquals(compositeAssociationRecord2, retrievedRecord2); + compositeAssociationRecords = check from CompositeAssociationRecord compositeAssociationRecord in + testEntitiesClient->/compositeassociationrecords.get(CompositeAssociationRecord) + select compositeAssociationRecord; + test:assertEquals(compositeAssociationRecords, [compositeAssociationRecordUpdated1]); + + check testEntitiesClient.close(); +} diff --git a/ballerina/tests/redis-workspace-tests.bal b/ballerina/tests/redis-workspace-tests.bal new file mode 100644 index 0000000..394a89e --- /dev/null +++ b/ballerina/tests/redis-workspace-tests.bal @@ -0,0 +1,203 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/persist; +import ballerina/test; + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisBuildingDeleteTestNegative] +} +function redisWorkspaceCreateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] workspaceIds = check rainierClient->/workspaces.post([workspace1]); + test:assertEquals(workspaceIds, [workspace1.workspaceId]); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId]; + test:assertEquals(workspaceRetrieved, workspace1); +} + +@test:Config { + groups: ["workspace", "redis"] +} +function redisWorkspaceCreateTest2() returns error? { + RedisRainierClient rainierClient = check new (); + + string[] workspaceIds = check rainierClient->/workspaces.post([workspace2, workspace3]); + + test:assertEquals(workspaceIds, [workspace2.workspaceId, workspace3.workspaceId]); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace2.workspaceId]; + test:assertEquals(workspaceRetrieved, workspace2); + + workspaceRetrieved = check rainierClient->/workspaces/[workspace3.workspaceId]; + test:assertEquals(workspaceRetrieved, workspace3); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceCreateTest] +} +function redisWorkspaceReadOneTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId]; + test:assertEquals(workspaceRetrieved, workspace1); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceCreateTest] +} +function redisWorkspaceReadOneDependentTest() returns error? { + RedisRainierClient rainierClient = check new (); + + WorkspaceInfo2 workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId]; + test:assertEquals(workspaceRetrieved, + { + workspaceType: workspace1.workspaceType, + locationBuildingCode: workspace1.locationBuildingCode + } + ); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceCreateTest] +} +function redisWorkspaceReadOneTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Workspace|error workspaceRetrieved = rainierClient->/workspaces/["invalid-workspace-id"]; + if workspaceRetrieved is persist:NotFoundError { + test:assertEquals(workspaceRetrieved.message(), + "A record with the key 'Workspace:invalid-workspace-id' does not exist for the entity 'Workspace'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceCreateTest, redisWorkspaceCreateTest2] +} +function redisWorkspaceReadManyTest() returns error? { + RedisRainierClient rainierClient = check new (); + + stream workspaceStream = rainierClient->/workspaces; + Workspace[] workspaces = check from Workspace workspace in workspaceStream + order by workspace.workspaceId ascending select workspace; + + test:assertEquals(workspaces, [workspace1, workspace2, workspace3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis", "dependent"], + dependsOn: [redisWorkspaceCreateTest, redisWorkspaceCreateTest2] +} +function redisWorkspaceReadManyDependentTest() returns error? { + RedisRainierClient rainierClient = check new (); + + stream workspaceStream = rainierClient->/workspaces; + WorkspaceInfo2[] workspaces = check from WorkspaceInfo2 workspace in workspaceStream + order by workspace.workspaceType select workspace; + + test:assertEquals(workspaces, [ + {workspaceType: workspace3.workspaceType, locationBuildingCode: workspace3.locationBuildingCode}, + {workspaceType: workspace2.workspaceType, locationBuildingCode: workspace2.locationBuildingCode}, + {workspaceType: workspace1.workspaceType, locationBuildingCode: workspace1.locationBuildingCode} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceReadOneTest, redisWorkspaceReadManyTest, redisWorkspaceReadManyDependentTest] +} +function redisWorkspaceUpdateTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Workspace workspace = check rainierClient->/workspaces/[workspace1.workspaceId].put({ + workspaceType: "large" + }); + + test:assertEquals(workspace, updatedWorkspace1); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId]; + test:assertEquals(workspaceRetrieved, updatedWorkspace1); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceReadOneTest, redisWorkspaceReadManyTest, redisWorkspaceReadManyDependentTest] +} +function redisWorkspaceUpdateTestNegative1() returns error? { + RedisRainierClient rainierClient = check new (); + + Workspace|error workspace = rainierClient->/workspaces/["invalid-workspace-id"].put({ + workspaceType: "large" + }); + + if workspace is persist:NotFoundError { + test:assertEquals(workspace.message(), + "A record with the key 'Workspace:invalid-workspace-id' does not exist for the entity 'Workspace'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceUpdateTest] +} +function redisWorkspaceDeleteTest() returns error? { + RedisRainierClient rainierClient = check new (); + + Workspace workspace = check rainierClient->/workspaces/[workspace1.workspaceId].delete(); + test:assertEquals(workspace, updatedWorkspace1); + + stream workspaceStream = rainierClient->/workspaces; + Workspace[] workspaces = check from Workspace workspace2 in workspaceStream + order by workspace2.workspaceId ascending select workspace2; + + test:assertEquals(workspaces, [workspace2, workspace3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "redis"], + dependsOn: [redisWorkspaceDeleteTest] +} +function redisWorkspaceDeleteTestNegative() returns error? { + RedisRainierClient rainierClient = check new (); + + Workspace|error workspace = rainierClient->/workspaces/[workspace1.workspaceId].delete(); + + if workspace is persist:NotFoundError { + test:assertEquals(workspace.message(), string `A record with the key 'Workspace:${workspace1.workspaceId}' does not exist for the entity 'Workspace'.`); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/redis_rainier_generated_client.bal b/ballerina/tests/redis_rainier_generated_client.bal new file mode 100644 index 0000000..d7547ad --- /dev/null +++ b/ballerina/tests/redis_rainier_generated_client.bal @@ -0,0 +1,394 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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; +import ballerina/persist; +import ballerinax/redis; + +const EMPLOYEE = "employees"; +const WORKSPACE = "workspaces"; +const BUILDING = "buildings"; +const DEPARTMENT = "departments"; +const ORDER_ITEM = "orderitems"; + +public isolated client class RedisRainierClient { + *persist:AbstractPersistClient; + + private final redis:Client dbClient; + + private final map persistClients; + + private final record {|RedisMetadata...;|} & readonly metadata = { + [EMPLOYEE] : { + entityName: "Employee", + collectionName: "Employee", + fieldMetadata: { + empNo: {fieldName: "empNo", fieldDataType: STRING}, + firstName: {fieldName: "firstName", fieldDataType: STRING}, + lastName: {fieldName: "lastName", fieldDataType: STRING}, + birthDate: {fieldName: "birthDate", fieldDataType: DATE}, + gender: {fieldName: "gender", fieldDataType: ENUM}, + hireDate: {fieldName: "hireDate", fieldDataType: DATE}, + departmentDeptNo: {fieldName: "departmentDeptNo", fieldDataType: STRING}, + workspaceWorkspaceId: {fieldName: "workspaceWorkspaceId", fieldDataType: STRING}, + "department.deptNo": {relation: {entityName: "department", refField: "deptNo", + refFieldDataType: STRING}}, + "department.deptName": {relation: {entityName: "department", refField: "deptName", + refFieldDataType: STRING}}, + "workspace.workspaceId": {relation: {entityName: "workspace", refField: "workspaceId", + refFieldDataType: STRING}}, + "workspace.workspaceType": {relation: {entityName: "workspace", refField: "workspaceType", + refFieldDataType: STRING}}, + "workspace.locationBuildingCode": {relation: {entityName: "workspace", + refField: "locationBuildingCode", refFieldDataType: STRING}} + }, + keyFields: ["empNo"], + refMetadata: { + department: {entity: Department, fieldName: "department", refCollection: "Department", + refFields: ["deptNo"], joinFields: ["departmentDeptNo"], 'type: ONE_TO_MANY}, + workspace: {entity: Workspace, fieldName: "workspace", refCollection: "Workspace", + refFields: ["workspaceId"], joinFields: ["workspaceWorkspaceId"], 'type: ONE_TO_MANY} + } + }, + [WORKSPACE] : { + entityName: "Workspace", + collectionName: "Workspace", + fieldMetadata: { + workspaceId: {fieldName: "workspaceId", fieldDataType: STRING}, + workspaceType: {fieldName: "workspaceType", fieldDataType: STRING}, + locationBuildingCode: {fieldName: "locationBuildingCode", fieldDataType: STRING}, + "location.buildingCode": {relation: {entityName: "location", refField: "buildingCode", + refFieldDataType: STRING}}, + "location.city": {relation: {entityName: "location", refField: "city", refFieldDataType: STRING}}, + "location.state": {relation: {entityName: "location", refField: "state", refFieldDataType: STRING}}, + "location.country": {relation: {entityName: "location", refField: "country", refFieldDataType: STRING}}, + "location.postalCode": {relation: {entityName: "location", refField: "postalCode", + refFieldDataType: STRING}}, + "location.type": {relation: {entityName: "location", refField: "type", refFieldDataType: STRING}}, + "employees[].empNo": {relation: {entityName: "employees", refField: "empNo", refFieldDataType: STRING}}, + "employees[].firstName": {relation: {entityName: "employees", refField: "firstName", + refFieldDataType: STRING}}, + "employees[].lastName": {relation: {entityName: "employees", refField: "lastName", + refFieldDataType: STRING}}, + "employees[].birthDate": {relation: {entityName: "employees", refField: "birthDate", + refFieldDataType: DATE}}, + "employees[].gender": {relation: {entityName: "employees", refField: "gender", refFieldDataType: ENUM}}, + "employees[].hireDate": {relation: {entityName: "employees", refField: "hireDate", + refFieldDataType: DATE}}, + "employees[].departmentDeptNo": {relation: {entityName: "employees", refField: "departmentDeptNo", + refFieldDataType: STRING}}, + "employees[].workspaceWorkspaceId": {relation: {entityName: "employees", + refField: "workspaceWorkspaceId", refFieldDataType: STRING}} + }, + keyFields: ["workspaceId"], + refMetadata: { + location: {entity: Building, fieldName: "location", refCollection: "Building", + refFields: ["buildingCode"], joinFields: ["locationBuildingCode"], 'type: ONE_TO_MANY}, + employees: {entity: Employee, fieldName: "employees", refCollection: "Employee", + refFields: ["workspaceWorkspaceId"], joinFields: ["workspaceId"], 'type: MANY_TO_ONE} + } + }, + [BUILDING] : { + entityName: "Building", + collectionName: "Building", + fieldMetadata: { + buildingCode: {fieldName: "buildingCode", fieldDataType: STRING}, + city: {fieldName: "city", fieldDataType: STRING}, + state: {fieldName: "state", fieldDataType: STRING}, + country: {fieldName: "country", fieldDataType: STRING}, + postalCode: {fieldName: "postalCode", fieldDataType: STRING}, + 'type: {fieldName: "type", fieldDataType: STRING}, + "workspaces[].workspaceId": {relation: {entityName: "workspaces", refField: "workspaceId", + refFieldDataType: STRING}}, + "workspaces[].workspaceType": {relation: {entityName: "workspaces", refField: "workspaceType", + refFieldDataType: STRING}}, + "workspaces[].locationBuildingCode": {relation: {entityName: "workspaces", + refField: "locationBuildingCode", refFieldDataType: STRING}} + }, + keyFields: ["buildingCode"], + refMetadata: {workspaces: {entity: Workspace, fieldName: "workspaces", refCollection: "Workspace", + refFields: ["locationBuildingCode"], joinFields: ["buildingCode"], 'type: MANY_TO_ONE}} + }, + [DEPARTMENT] : { + entityName: "Department", + collectionName: "Department", + fieldMetadata: { + deptNo: {fieldName: "deptNo", fieldDataType: STRING}, + deptName: {fieldName: "deptName", fieldDataType: STRING}, + "employees[].empNo": {relation: {entityName: "employees", refField: "empNo", refFieldDataType: STRING}}, + "employees[].firstName": {relation: {entityName: "employees", refField: "firstName", + refFieldDataType: STRING}}, + "employees[].lastName": {relation: {entityName: "employees", refField: "lastName", + refFieldDataType: STRING}}, + "employees[].birthDate": {relation: {entityName: "employees", refField: "birthDate", + refFieldDataType: DATE}}, + "employees[].gender": {relation: {entityName: "employees", refField: "gender", refFieldDataType: ENUM}}, + "employees[].hireDate": {relation: {entityName: "employees", refField: "hireDate", + refFieldDataType: DATE}}, + "employees[].departmentDeptNo": {relation: {entityName: "employees", refField: "departmentDeptNo", + refFieldDataType: STRING}}, + "employees[].workspaceWorkspaceId": {relation: {entityName: "employees", + refField: "workspaceWorkspaceId", refFieldDataType: STRING}} + }, + keyFields: ["deptNo"], + refMetadata: {employees: {entity: Employee, fieldName: "employees", refCollection: "Employee", + refFields: ["departmentDeptNo"], joinFields: ["deptNo"], 'type: MANY_TO_ONE}} + }, + [ORDER_ITEM] : { + entityName: "OrderItem", + collectionName: "OrderItem", + fieldMetadata: { + orderId: {fieldName: "orderId", fieldDataType: STRING}, + itemId: {fieldName: "itemId", fieldDataType: STRING}, + quantity: {fieldName: "quantity", fieldDataType: INT}, + notes: {fieldName: "notes", fieldDataType: STRING} + }, + keyFields: ["orderId", "itemId"] + } + }; + + public isolated function init() returns persist:Error? { + redis:Client|error dbClient = new (redis); + if dbClient is error { + return error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [EMPLOYEE] : check new (dbClient, self.metadata.get(EMPLOYEE)), + [WORKSPACE] : check new (dbClient, self.metadata.get(WORKSPACE)), + [BUILDING] : check new (dbClient, self.metadata.get(BUILDING)), + [DEPARTMENT] : check new (dbClient, self.metadata.get(DEPARTMENT)), + [ORDER_ITEM] : check new (dbClient, self.metadata.get(ORDER_ITEM)) + }; + } + + isolated resource function get employees(EmployeeTargetType targetType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get employees/[string empNo](EmployeeTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post employees(EmployeeInsert[] data) returns string[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(EMPLOYEE); + } + _ = check redisClient.runBatchInsertQuery(data); + return from EmployeeInsert inserted in data + select inserted.empNo; + } + + isolated resource function put employees/[string empNo](EmployeeUpdate value) returns Employee|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(EMPLOYEE); + } + _ = check redisClient.runUpdateQuery(empNo, value); + return self->/employees/[empNo]; + } + + isolated resource function delete employees/[string empNo]() returns Employee|persist:Error { + Employee result = check self->/employees/[empNo]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(EMPLOYEE); + } + _ = check redisClient.runDeleteQuery(empNo); + return result; + } + + isolated resource function get workspaces(WorkspaceTargetType targetType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get workspaces/[string workspaceId](WorkspaceTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post workspaces(WorkspaceInsert[] data) returns string[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(WORKSPACE); + } + _ = check redisClient.runBatchInsertQuery(data); + return from WorkspaceInsert inserted in data + select inserted.workspaceId; + } + + isolated resource function put workspaces/[string workspaceId](WorkspaceUpdate value) + returns Workspace|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(WORKSPACE); + } + _ = check redisClient.runUpdateQuery(workspaceId, value); + return self->/workspaces/[workspaceId]; + } + + isolated resource function delete workspaces/[string workspaceId]() returns Workspace|persist:Error { + Workspace result = check self->/workspaces/[workspaceId]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(WORKSPACE); + } + _ = check redisClient.runDeleteQuery(workspaceId); + return result; + } + + isolated resource function get buildings(BuildingTargetType targetType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get buildings/[string buildingCode](BuildingTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post buildings(BuildingInsert[] data) returns string[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(BUILDING); + } + _ = check redisClient.runBatchInsertQuery(data); + return from BuildingInsert inserted in data + select inserted.buildingCode; + } + + isolated resource function put buildings/[string buildingCode](BuildingUpdate value) + returns Building|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(BUILDING); + } + _ = check redisClient.runUpdateQuery(buildingCode, value); + return self->/buildings/[buildingCode]; + } + + isolated resource function delete buildings/[string buildingCode]() returns Building|persist:Error { + Building result = check self->/buildings/[buildingCode]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(BUILDING); + } + _ = check redisClient.runDeleteQuery(buildingCode); + return result; + } + + isolated resource function get departments(DepartmentTargetType targetType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get departments/[string deptNo](DepartmentTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post departments(DepartmentInsert[] data) returns string[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(DEPARTMENT); + } + _ = check redisClient.runBatchInsertQuery(data); + return from DepartmentInsert inserted in data + select inserted.deptNo; + } + + isolated resource function put departments/[string deptNo](DepartmentUpdate value) + returns Department|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(DEPARTMENT); + } + _ = check redisClient.runUpdateQuery(deptNo, value); + return self->/departments/[deptNo]; + } + + isolated resource function delete departments/[string deptNo]() returns Department|persist:Error { + Department result = check self->/departments/[deptNo]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(DEPARTMENT); + } + _ = check redisClient.runDeleteQuery(deptNo); + return result; + } + + isolated resource function get orderitems(OrderItemTargetType targetType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get orderitems/[string orderId]/[string itemId](OrderItemTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post orderitems(OrderItemInsert[] data) returns [string, string][]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ORDER_ITEM); + } + _ = check redisClient.runBatchInsertQuery(data); + return from OrderItemInsert inserted in data + select [inserted.orderId, inserted.itemId]; + } + + isolated resource function put orderitems/[string orderId]/[string itemId](OrderItemUpdate value) + returns OrderItem|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ORDER_ITEM); + } + _ = check redisClient.runUpdateQuery({"orderId": orderId, "itemId": itemId}, value); + return self->/orderitems/[orderId]/[itemId]; + } + + isolated resource function delete orderitems/[string orderId]/[string itemId]() returns OrderItem|persist:Error { + OrderItem result = check self->/orderitems/[orderId]/[itemId]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ORDER_ITEM); + } + _ = check redisClient.runDeleteQuery({"orderId": orderId, "itemId": itemId}); + return result; + } + + public isolated function close() returns persist:Error? { + error? result = self.dbClient.close(); + if result is error { + return error(result.message()); + } + return result; + } +} diff --git a/ballerina/tests/redis_test_entities_generated_client.bal b/ballerina/tests/redis_test_entities_generated_client.bal new file mode 100644 index 0000000..5bfafea --- /dev/null +++ b/ballerina/tests/redis_test_entities_generated_client.bal @@ -0,0 +1,551 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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; +import ballerina/persist; +import ballerinax/redis; + +const ALL_TYPES = "alltypes"; +const STRING_ID_RECORD = "stringidrecords"; +const INT_ID_RECORD = "intidrecords"; +const FLOAT_ID_RECORD = "floatidrecords"; +const DECIMAL_ID_RECORD = "decimalidrecords"; +const BOOLEAN_ID_RECORD = "booleanidrecords"; +const COMPOSITE_ASSOCIATION_RECORD = "compositeassociationrecords"; +const ALL_TYPES_ID_RECORD = "alltypesidrecords"; + +public isolated client class RedisTestEntitiesClient { + *persist:AbstractPersistClient; + + private final redis:Client dbClient; + + private final map persistClients; + + private final record {|RedisMetadata...;|} & readonly metadata = { + [ALL_TYPES] : { + entityName: "AllTypes", + collectionName: "AllTypes", + fieldMetadata: { + id: {fieldName: "id", fieldDataType: INT}, + booleanType: {fieldName: "booleanType", fieldDataType: BOOLEAN}, + intType: {fieldName: "intType", fieldDataType: INT}, + floatType: {fieldName: "floatType", fieldDataType: FLOAT}, + decimalType: {fieldName: "decimalType", fieldDataType: DECIMAL}, + stringType: {fieldName: "stringType", fieldDataType: STRING}, + dateType: {fieldName: "dateType", fieldDataType: DATE}, + timeOfDayType: {fieldName: "timeOfDayType", fieldDataType: TIME_OF_DAY}, + utcType: {fieldName: "utcType", fieldDataType: UTC}, + civilType: {fieldName: "civilType", fieldDataType: CIVIL}, + booleanTypeOptional: {fieldName: "booleanTypeOptional", fieldDataType: BOOLEAN}, + intTypeOptional: {fieldName: "intTypeOptional", fieldDataType: INT}, + floatTypeOptional: {fieldName: "floatTypeOptional", fieldDataType: FLOAT}, + decimalTypeOptional: {fieldName: "decimalTypeOptional", fieldDataType: DECIMAL}, + stringTypeOptional: {fieldName: "stringTypeOptional", fieldDataType: STRING}, + dateTypeOptional: {fieldName: "dateTypeOptional", fieldDataType: DATE}, + timeOfDayTypeOptional: {fieldName: "timeOfDayTypeOptional", fieldDataType: TIME_OF_DAY}, + utcTypeOptional: {fieldName: "utcTypeOptional", fieldDataType: UTC}, + civilTypeOptional: {fieldName: "civilTypeOptional", fieldDataType: CIVIL}, + enumType: {fieldName: "enumType", fieldDataType: ENUM}, + enumTypeOptional: {fieldName: "enumTypeOptional", fieldDataType: ENUM} + }, + keyFields: ["id"] + }, + [STRING_ID_RECORD] : { + entityName: "StringIdRecord", + collectionName: "StringIdRecord", + fieldMetadata: { + id: {fieldName: "id", fieldDataType: STRING}, + randomField: {fieldName: "randomField", fieldDataType: STRING} + }, + keyFields: ["id"] + }, + [INT_ID_RECORD] : { + entityName: "IntIdRecord", + collectionName: "IntIdRecord", + fieldMetadata: { + id: {fieldName: "id", fieldDataType: INT}, + randomField: {fieldName: "randomField", fieldDataType: STRING} + }, + keyFields: ["id"] + }, + [FLOAT_ID_RECORD] : { + entityName: "FloatIdRecord", + collectionName: "FloatIdRecord", + fieldMetadata: { + id: {fieldName: "id", fieldDataType: FLOAT}, + randomField: {fieldName: "randomField", fieldDataType: STRING} + }, + keyFields: ["id"] + }, + [DECIMAL_ID_RECORD] : { + entityName: "DecimalIdRecord", + collectionName: "DecimalIdRecord", + fieldMetadata: { + id: {fieldName: "id", fieldDataType: DECIMAL}, + randomField: {fieldName: "randomField", fieldDataType: STRING} + }, + keyFields: ["id"] + }, + [BOOLEAN_ID_RECORD] : { + entityName: "BooleanIdRecord", + collectionName: "BooleanIdRecord", + fieldMetadata: { + id: {fieldName: "id", fieldDataType: BOOLEAN}, + randomField: {fieldName: "randomField", fieldDataType: STRING} + }, + keyFields: ["id"] + }, + [COMPOSITE_ASSOCIATION_RECORD] : { + entityName: "CompositeAssociationRecord", + collectionName: "CompositeAssociationRecord", + fieldMetadata: { + id: {fieldName: "id", fieldDataType: STRING}, + randomField: {fieldName: "randomField", fieldDataType: STRING}, + alltypesidrecordBooleanType: {fieldName: "alltypesidrecordBooleanType", fieldDataType: BOOLEAN}, + alltypesidrecordIntType: {fieldName: "alltypesidrecordIntType", fieldDataType: INT}, + alltypesidrecordFloatType: {fieldName: "alltypesidrecordFloatType", fieldDataType: FLOAT}, + alltypesidrecordDecimalType: {fieldName: "alltypesidrecordDecimalType", fieldDataType: DECIMAL}, + alltypesidrecordStringType: {fieldName: "alltypesidrecordStringType", fieldDataType: STRING}, + "allTypesIdRecord.booleanType": {relation: {entityName: "allTypesIdRecord", refField: "booleanType", + refFieldDataType: BOOLEAN}}, + "allTypesIdRecord.intType": {relation: {entityName: "allTypesIdRecord", refField: "intType", + refFieldDataType: INT}}, + "allTypesIdRecord.floatType": {relation: {entityName: "allTypesIdRecord", refField: "floatType", + refFieldDataType: FLOAT}}, + "allTypesIdRecord.decimalType": {relation: {entityName: "allTypesIdRecord", refField: "decimalType", + refFieldDataType: DECIMAL}}, + "allTypesIdRecord.stringType": {relation: {entityName: "allTypesIdRecord", refField: "stringType", + refFieldDataType: STRING}}, + "allTypesIdRecord.randomField": {relation: {entityName: "allTypesIdRecord", refField: "randomField", + refFieldDataType: STRING}} + }, + keyFields: ["id"], + refMetadata: {allTypesIdRecord: {entity: AllTypesIdRecord, fieldName: "allTypesIdRecord", + refCollection: "AllTypesIdRecord", refFields: ["booleanType", "intType", "floatType", "decimalType", + "stringType"], joinFields: ["alltypesidrecordBooleanType", "alltypesidrecordIntType", + "alltypesidrecordFloatType", "alltypesidrecordDecimalType", "alltypesidrecordStringType"], + 'type: ONE_TO_ONE}} + }, + [ALL_TYPES_ID_RECORD] : { + entityName: "AllTypesIdRecord", + collectionName: "AllTypesIdRecord", + fieldMetadata: { + booleanType: {fieldName: "booleanType", fieldDataType: BOOLEAN}, + intType: {fieldName: "intType", fieldDataType: INT}, + floatType: {fieldName: "floatType", fieldDataType: FLOAT}, + decimalType: {fieldName: "decimalType", fieldDataType: DECIMAL}, + stringType: {fieldName: "stringType", fieldDataType: STRING}, + randomField: {fieldName: "randomField", fieldDataType: STRING}, + "compositeAssociationRecord.id": {relation: {entityName: "compositeAssociationRecord", refField: "id", + refFieldDataType: STRING}}, + "compositeAssociationRecord.randomField": {relation: {entityName: "compositeAssociationRecord", + refField: "randomField", refFieldDataType: STRING}}, + "compositeAssociationRecord.alltypesidrecordBooleanType": {relation: { + entityName: "compositeAssociationRecord", refField: "alltypesidrecordBooleanType", + refFieldDataType: BOOLEAN}}, + "compositeAssociationRecord.alltypesidrecordIntType": {relation: { + entityName: "compositeAssociationRecord", refField: "alltypesidrecordIntType", + refFieldDataType: INT}}, + "compositeAssociationRecord.alltypesidrecordFloatType": {relation: { + entityName: "compositeAssociationRecord", refField: "alltypesidrecordFloatType", + refFieldDataType: FLOAT}}, + "compositeAssociationRecord.alltypesidrecordDecimalType": {relation: { + entityName: "compositeAssociationRecord", refField: "alltypesidrecordDecimalType", + refFieldDataType: DECIMAL}}, + "compositeAssociationRecord.alltypesidrecordStringType": {relation: { + entityName: "compositeAssociationRecord", refField: "alltypesidrecordStringType", + refFieldDataType: STRING}} + }, + keyFields: ["booleanType", "intType", "floatType", "decimalType", "stringType"], + refMetadata: {compositeAssociationRecord: {entity: CompositeAssociationRecord, + fieldName: "compositeAssociationRecord", refCollection: "CompositeAssociationRecord", + refFields: ["alltypesidrecordBooleanType", "alltypesidrecordIntType", "alltypesidrecordFloatType", + "alltypesidrecordDecimalType", "alltypesidrecordStringType"], joinFields: ["booleanType", "intType", + "floatType", "decimalType", "stringType"], 'type: ONE_TO_ONE}} + } + }; + + public isolated function init() returns persist:Error? { + redis:Client|error dbClient = new (redis); + if dbClient is error { + return error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [ALL_TYPES] : check new (dbClient, self.metadata.get(ALL_TYPES)), + [STRING_ID_RECORD] : check new (dbClient, self.metadata.get(STRING_ID_RECORD)), + [INT_ID_RECORD] : check new (dbClient, self.metadata.get(INT_ID_RECORD)), + [FLOAT_ID_RECORD] : check new (dbClient, self.metadata.get(FLOAT_ID_RECORD)), + [DECIMAL_ID_RECORD] : check new (dbClient, self.metadata.get(DECIMAL_ID_RECORD)), + [BOOLEAN_ID_RECORD] : check new (dbClient, self.metadata.get(BOOLEAN_ID_RECORD)), + [COMPOSITE_ASSOCIATION_RECORD] : check new (dbClient, self.metadata.get(COMPOSITE_ASSOCIATION_RECORD)), + [ALL_TYPES_ID_RECORD] : check new (dbClient, self.metadata.get(ALL_TYPES_ID_RECORD)) + }; + } + + isolated resource function get alltypes(AllTypesTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get alltypes/[int id](AllTypesTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post alltypes(AllTypesInsert[] data) returns int[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ALL_TYPES); + } + _ = check redisClient.runBatchInsertQuery(data); + return from AllTypesInsert inserted in data + select inserted.id; + } + + isolated resource function put alltypes/[int id](AllTypesUpdate value) returns AllTypes|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ALL_TYPES); + } + _ = check redisClient.runUpdateQuery(id, value); + return self->/alltypes/[id]; + } + + isolated resource function delete alltypes/[int id]() returns AllTypes|persist:Error { + AllTypes result = check self->/alltypes/[id]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ALL_TYPES); + } + _ = check redisClient.runDeleteQuery(id); + return result; + } + + isolated resource function get stringidrecords(StringIdRecordTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get stringidrecords/[string id](StringIdRecordTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post stringidrecords(StringIdRecordInsert[] data) returns string[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(STRING_ID_RECORD); + } + _ = check redisClient.runBatchInsertQuery(data); + return from StringIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put stringidrecords/[string id](StringIdRecordUpdate value) + returns StringIdRecord|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(STRING_ID_RECORD); + } + _ = check redisClient.runUpdateQuery(id, value); + return self->/stringidrecords/[id]; + } + + isolated resource function delete stringidrecords/[string id]() returns StringIdRecord|persist:Error { + StringIdRecord result = check self->/stringidrecords/[id]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(STRING_ID_RECORD); + } + _ = check redisClient.runDeleteQuery(id); + return result; + } + + isolated resource function get intidrecords(IntIdRecordTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get intidrecords/[int id](IntIdRecordTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post intidrecords(IntIdRecordInsert[] data) returns int[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(INT_ID_RECORD); + } + _ = check redisClient.runBatchInsertQuery(data); + return from IntIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put intidrecords/[int id](IntIdRecordUpdate value) returns IntIdRecord|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(INT_ID_RECORD); + } + _ = check redisClient.runUpdateQuery(id, value); + return self->/intidrecords/[id]; + } + + isolated resource function delete intidrecords/[int id]() returns IntIdRecord|persist:Error { + IntIdRecord result = check self->/intidrecords/[id]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(INT_ID_RECORD); + } + _ = check redisClient.runDeleteQuery(id); + return result; + } + + isolated resource function get floatidrecords(FloatIdRecordTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get floatidrecords/[float id](FloatIdRecordTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post floatidrecords(FloatIdRecordInsert[] data) returns float[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(FLOAT_ID_RECORD); + } + _ = check redisClient.runBatchInsertQuery(data); + return from FloatIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put floatidrecords/[float id](FloatIdRecordUpdate value) + returns FloatIdRecord|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(FLOAT_ID_RECORD); + } + _ = check redisClient.runUpdateQuery(id, value); + return self->/floatidrecords/[id]; + } + + isolated resource function delete floatidrecords/[float id]() returns FloatIdRecord|persist:Error { + FloatIdRecord result = check self->/floatidrecords/[id]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(FLOAT_ID_RECORD); + } + _ = check redisClient.runDeleteQuery(id); + return result; + } + + isolated resource function get decimalidrecords(DecimalIdRecordTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get decimalidrecords/[decimal id](DecimalIdRecordTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post decimalidrecords(DecimalIdRecordInsert[] data) returns decimal[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(DECIMAL_ID_RECORD); + } + _ = check redisClient.runBatchInsertQuery(data); + return from DecimalIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put decimalidrecords/[decimal id](DecimalIdRecordUpdate value) + returns DecimalIdRecord|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(DECIMAL_ID_RECORD); + } + _ = check redisClient.runUpdateQuery(id, value); + return self->/decimalidrecords/[id]; + } + + isolated resource function delete decimalidrecords/[decimal id]() returns DecimalIdRecord|persist:Error { + DecimalIdRecord result = check self->/decimalidrecords/[id]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(DECIMAL_ID_RECORD); + } + _ = check redisClient.runDeleteQuery(id); + return result; + } + + isolated resource function get booleanidrecords(BooleanIdRecordTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get booleanidrecords/[boolean id](BooleanIdRecordTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post booleanidrecords(BooleanIdRecordInsert[] data) returns boolean[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(BOOLEAN_ID_RECORD); + } + _ = check redisClient.runBatchInsertQuery(data); + return from BooleanIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put booleanidrecords/[boolean id](BooleanIdRecordUpdate value) + returns BooleanIdRecord|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(BOOLEAN_ID_RECORD); + } + _ = check redisClient.runUpdateQuery(id, value); + return self->/booleanidrecords/[id]; + } + + isolated resource function delete booleanidrecords/[boolean id]() returns BooleanIdRecord|persist:Error { + BooleanIdRecord result = check self->/booleanidrecords/[id]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(BOOLEAN_ID_RECORD); + } + _ = check redisClient.runDeleteQuery(id); + return result; + } + + isolated resource function get compositeassociationrecords(CompositeAssociationRecordTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get + compositeassociationrecords/[string id](CompositeAssociationRecordTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post compositeassociationrecords(CompositeAssociationRecordInsert[] data) + returns string[]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(COMPOSITE_ASSOCIATION_RECORD); + } + _ = check redisClient.runBatchInsertQuery(data); + return from CompositeAssociationRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put compositeassociationrecords/[string id](CompositeAssociationRecordUpdate value) + returns CompositeAssociationRecord|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(COMPOSITE_ASSOCIATION_RECORD); + } + _ = check redisClient.runUpdateQuery(id, value); + return self->/compositeassociationrecords/[id]; + } + + isolated resource function delete compositeassociationrecords/[string id]() + returns CompositeAssociationRecord|persist:Error { + CompositeAssociationRecord result = check self->/compositeassociationrecords/[id]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(COMPOSITE_ASSOCIATION_RECORD); + } + _ = check redisClient.runDeleteQuery(id); + return result; + } + + isolated resource function get alltypesidrecords(AllTypesIdRecordTargetType targetType = <>) + returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "query" + } external; + + isolated resource function get alltypesidrecords/[boolean booleanType]/[int intType]/[float floatType] + /[decimal decimalType]/[string stringType](AllTypesIdRecordTargetType targetType = <>) + returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.redis.datastore.RedisProcessor", + name: "queryOne" + } external; + + isolated resource function post alltypesidrecords(AllTypesIdRecordInsert[] data) returns [boolean, int, float, + decimal, string][]|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ALL_TYPES_ID_RECORD); + } + _ = check redisClient.runBatchInsertQuery(data); + return from AllTypesIdRecordInsert inserted in data + select [inserted.booleanType, inserted.intType, inserted.floatType, inserted.decimalType, + inserted.stringType]; + } + + isolated resource function put alltypesidrecords/[boolean booleanType]/[int intType]/[float floatType] + /[decimal decimalType]/[string stringType](AllTypesIdRecordUpdate value) returns AllTypesIdRecord|persist:Error { + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ALL_TYPES_ID_RECORD); + } + _ = check redisClient.runUpdateQuery({"booleanType": booleanType, "intType": intType, "floatType": + floatType, "decimalType": decimalType, "stringType": stringType}, value); + return self->/alltypesidrecords/[booleanType]/[intType]/[floatType]/[decimalType]/[stringType]; + } + + isolated resource function delete + alltypesidrecords/[boolean booleanType]/[int intType]/[float floatType]/[decimal decimalType]/[string stringType]() + returns AllTypesIdRecord|persist:Error { + AllTypesIdRecord result = + check self->/alltypesidrecords/[booleanType]/[intType]/[floatType]/[decimalType]/[stringType]; + RedisClient redisClient; + lock { + redisClient = self.persistClients.get(ALL_TYPES_ID_RECORD); + } + _ = check redisClient.runDeleteQuery({"booleanType": booleanType, "intType": intType, "floatType": + floatType, "decimalType": decimalType, "stringType": stringType}); + return result; + } + + public isolated function close() returns persist:Error? { + error? result = self.dbClient.close(); + if result is error { + return error(result.message()); + } + return result; + } +} diff --git a/ballerina/tests/resources/Dockerfile b/ballerina/tests/resources/Dockerfile new file mode 100644 index 0000000..0d7649c --- /dev/null +++ b/ballerina/tests/resources/Dockerfile @@ -0,0 +1,18 @@ +# Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + +FROM redis:latest +EXPOSE 6379 diff --git a/ballerina/tests/test_entities_generated_types.bal b/ballerina/tests/test_entities_generated_types.bal new file mode 100644 index 0000000..0588800 --- /dev/null +++ b/ballerina/tests/test_entities_generated_types.bal @@ -0,0 +1,301 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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/time; + +public enum EnumType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4 +} + +public enum OrderType { + ONLINE, + INSTORE +} + +public type AllTypes record {| + readonly int id; + boolean booleanType; + int intType; + float floatType; + decimal decimalType; + string stringType; + time:Date dateType; + time:TimeOfDay timeOfDayType; + time:Utc utcType; + time:Civil civilType; + EnumType enumType; + boolean booleanTypeOptional?; + int intTypeOptional?; + float floatTypeOptional?; + decimal decimalTypeOptional?; + string stringTypeOptional?; + time:Date dateTypeOptional?; + time:TimeOfDay timeOfDayTypeOptional?; + time:Utc utcTypeOptional?; + time:Civil civilTypeOptional?; + EnumType enumTypeOptional?; +|}; + +public type AllTypesOptionalized record {| + int id?; + boolean booleanType?; + int intType?; + float floatType?; + decimal decimalType?; + string stringType?; + time:Date dateType?; + time:TimeOfDay timeOfDayType?; + time:Utc utcType?; + time:Civil civilType?; + EnumType enumType?; + boolean booleanTypeOptional?; + int intTypeOptional?; + float floatTypeOptional?; + decimal decimalTypeOptional?; + string stringTypeOptional?; + time:Date dateTypeOptional?; + time:TimeOfDay timeOfDayTypeOptional?; + time:Utc utcTypeOptional?; + time:Civil civilTypeOptional?; + EnumType enumTypeOptional?; +|}; + +public type AllTypesTargetType typedesc; + +public type AllTypesInsert AllTypes; + +public type AllTypesUpdate record {| + boolean booleanType?; + int intType?; + float floatType?; + decimal decimalType?; + string stringType?; + time:Date dateType?; + time:TimeOfDay timeOfDayType?; + time:Utc utcType?; + time:Civil civilType?; + EnumType enumType?; + boolean booleanTypeOptional?; + int intTypeOptional?; + float floatTypeOptional?; + decimal decimalTypeOptional?; + string stringTypeOptional?; + time:Date dateTypeOptional?; + time:TimeOfDay timeOfDayTypeOptional?; + time:Utc utcTypeOptional?; + time:Civil civilTypeOptional?; + EnumType enumTypeOptional?; +|}; + +public type StringIdRecord record {| + readonly string id; + string randomField; +|}; + +public type StringIdRecordOptionalized record {| + string id?; + string randomField?; +|}; + +public type StringIdRecordTargetType typedesc; + +public type StringIdRecordInsert StringIdRecord; + +public type StringIdRecordUpdate record {| + string randomField?; +|}; + +public type IntIdRecord record {| + readonly int id; + string randomField; +|}; + +public type IntIdRecordOptionalized record {| + int id?; + string randomField?; +|}; + +public type IntIdRecordTargetType typedesc; + +public type IntIdRecordInsert IntIdRecord; + +public type IntIdRecordUpdate record {| + string randomField?; +|}; + +public type FloatIdRecord record {| + readonly float id; + string randomField; +|}; + +public type FloatIdRecordOptionalized record {| + float id?; + string randomField?; +|}; + +public type FloatIdRecordTargetType typedesc; + +public type FloatIdRecordInsert FloatIdRecord; + +public type FloatIdRecordUpdate record {| + string randomField?; +|}; + +public type DecimalIdRecord record {| + readonly decimal id; + string randomField; +|}; + +public type DecimalIdRecordOptionalized record {| + decimal id?; + string randomField?; +|}; + +public type DecimalIdRecordTargetType typedesc; + +public type DecimalIdRecordInsert DecimalIdRecord; + +public type DecimalIdRecordUpdate record {| + string randomField?; +|}; + +public type BooleanIdRecord record {| + readonly boolean id; + string randomField; +|}; + +public type BooleanIdRecordOptionalized record {| + boolean id?; + string randomField?; +|}; + +public type BooleanIdRecordTargetType typedesc; + +public type BooleanIdRecordInsert BooleanIdRecord; + +public type BooleanIdRecordUpdate record {| + string randomField?; +|}; + +public type CompositeAssociationRecord record {| + readonly string id; + string randomField; + boolean alltypesidrecordBooleanType; + int alltypesidrecordIntType; + float alltypesidrecordFloatType; + decimal alltypesidrecordDecimalType; + string alltypesidrecordStringType; +|}; + +public type CompositeAssociationRecordOptionalized record {| + string id?; + string randomField?; + boolean alltypesidrecordBooleanType?; + int alltypesidrecordIntType?; + float alltypesidrecordFloatType?; + decimal alltypesidrecordDecimalType?; + string alltypesidrecordStringType?; +|}; + +public type CompositeAssociationRecordWithRelations record {| + *CompositeAssociationRecordOptionalized; + AllTypesIdRecordOptionalized allTypesIdRecord?; +|}; + +public type CompositeAssociationRecordTargetType typedesc; + +public type CompositeAssociationRecordInsert CompositeAssociationRecord; + +public type CompositeAssociationRecordUpdate record {| + string randomField?; + boolean alltypesidrecordBooleanType?; + int alltypesidrecordIntType?; + float alltypesidrecordFloatType?; + decimal alltypesidrecordDecimalType?; + string alltypesidrecordStringType?; +|}; + +public type AllTypesIdRecord record {| + readonly boolean booleanType; + readonly int intType; + readonly float floatType; + readonly decimal decimalType; + readonly string stringType; + string randomField; +|}; + +public type AllTypesIdRecordOptionalized record {| + boolean booleanType?; + int intType?; + float floatType?; + decimal decimalType?; + string stringType?; + string randomField?; +|}; + +public type AllTypesIdRecordWithRelations record {| + *AllTypesIdRecordOptionalized; + CompositeAssociationRecordOptionalized compositeAssociationRecord?; +|}; + +public type AllTypesIdRecordTargetType typedesc; + +public type AllTypesIdRecordInsert AllTypesIdRecord; + +public type AllTypesIdRecordUpdate record {| + string randomField?; +|}; + +public type OrderItemExtended record {| + readonly string orderId; + readonly string itemId; + int CustomerId; + boolean paid; + float ammountPaid; + decimal ammountPaidDecimal; + time:Date arivalTimeDate; + time:TimeOfDay arivalTimeTimeOfDay; + OrderType orderType; +|}; + +public type OrderItemExtendedOptionalized record {| + string orderId?; + string itemId?; + int CustomerId?; + boolean paid?; + float ammountPaid?; + decimal ammountPaidDecimal?; + time:Date arivalTimeDate?; + time:TimeOfDay arivalTimeTimeOfDay?; + OrderType orderType?; +|}; + +public type OrderItemExtendedTargetType typedesc; + +public type OrderItemExtendedInsert OrderItemExtended; + +public type OrderItemExtendedUpdate record {| + int CustomerId?; + boolean paid?; + float ammountPaid?; + decimal ammountPaidDecimal?; + time:Date arivalTimeDate?; + time:TimeOfDay arivalTimeTimeOfDay?; + OrderType orderType?; +|}; diff --git a/build-config/checkstyle/build.gradle b/build-config/checkstyle/build.gradle new file mode 100644 index 0000000..4d81736 --- /dev/null +++ b/build-config/checkstyle/build.gradle @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +plugins { + id "de.undercouch.download" +} + +apply plugin: 'java' + +task downloadCheckstyleRuleFiles(type: Download) { + src([ + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/checkstyle.xml', + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/suppressions.xml' + ]) + overwrite false + onlyIfNewer true + dest buildDir +} + +jar { + enabled = false +} + +clean { + enabled = false +} + +artifacts.add('default', file("$project.buildDir/checkstyle.xml")) { + builtBy('downloadCheckstyleRuleFiles') +} + +artifacts.add('default', file("$project.buildDir/suppressions.xml")) { + builtBy('downloadCheckstyleRuleFiles') +} diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml new file mode 100644 index 0000000..7be879e --- /dev/null +++ b/build-config/resources/Ballerina.toml @@ -0,0 +1,24 @@ +[package] +org = "ballerinax" +name = "persist.redis" +version = "@toml.version@" +authors = ["Ballerina"] +keywords = ["persist", "redis", "experimental"] +repository = "https://github.com/ballerina-platform/module-ballerinax-persist.redis" +license = ["Apache-2.0"] +distribution = "2201.9.0" + +[platform.java17] +graalvmCompatible = true + +[[platform.java17.dependency]] +groupId = "io.ballerina.persist" +artifactId = "persist-redis-native" +version = "@toml.version@" +path = "../native/build/libs/persist.redis-native-@project.version@.jar" + +[[platform.java17.dependency]] +groupId = "io.ballerina.stdlib" +artifactId = "persist-native" +version = "@persist.version@" +path = "./lib/persist-native-@persist.native.version@.jar" diff --git a/build-config/spotbugs-exclude.xml b/build-config/spotbugs-exclude.xml new file mode 100644 index 0000000..43bc332 --- /dev/null +++ b/build-config/spotbugs-exclude.xml @@ -0,0 +1,2 @@ + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..031e3dd --- /dev/null +++ b/build.gradle @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +plugins { + id 'com.github.spotbugs' + id 'com.github.johnrengelman.shadow' + id 'de.undercouch.download' + id 'net.researchgate.release' + id 'jacoco' +} + +description = 'Ballerina - Persist' + +def packageName = "persist.redis" + +ballerinaLangVersion = project.ballerinaLangVersion + +allprojects { + group = project.group + version = project.version + + apply plugin: 'jacoco' + 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.*\$' + } +} + +subprojects { + + configurations { + externalJars + ballerinaStdLibs + } + + dependencies { + ballerinaStdLibs "io.ballerina.stdlib:io-ballerina:${stdlibIoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:time-ballerina:${stdlibTimeVersion}" + ballerinaStdLibs "io.ballerina.stdlib:url-ballerina:${stdlibUrlVersion}" + ballerinaStdLibs "io.ballerina.stdlib:constraint-ballerina:${stdlibConstraintVersion}" + ballerinaStdLibs "io.ballerina.stdlib:task-ballerina:${stdlibTaskVersion}" + ballerinaStdLibs "io.ballerina.stdlib:crypto-ballerina:${stdlibCryptoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:os-ballerina:${stdlibOsVersion}" + ballerinaStdLibs "io.ballerina.stdlib:log-ballerina:${stdlibLogVersion}" + ballerinaStdLibs "io.ballerina.stdlib:mime-ballerina:${stdlibMimeVersion}" + ballerinaStdLibs "io.ballerina.stdlib:file-ballerina:${stdlibFileVersion}" + ballerinaStdLibs "io.ballerina.stdlib:uuid-ballerina:${stdlibUuidVersion}" + ballerinaStdLibs "io.ballerina.stdlib:cache-ballerina:${stdlibCacheVersion}" + ballerinaStdLibs "io.ballerina.stdlib:oauth2-ballerina:${stdlibOAuth2Version}" + ballerinaStdLibs "io.ballerina.stdlib:auth-ballerina:${stdlibAuthVersion}" + ballerinaStdLibs "io.ballerina.stdlib:jwt-ballerina:${stdlibJwtVersion}" + ballerinaStdLibs "io.ballerina.stdlib:http-ballerina:${stdlibHttpVersion}" + ballerinaStdLibs "io.ballerina.stdlib:persist-ballerina:${stdlibPersistVersion}" + ballerinaStdLibs "io.ballerina.stdlib:observe-ballerina:${observeVersion}" + ballerinaStdLibs "io.ballerina:observe-ballerina:${observeInternalVersion}" + } +} + +def moduleVersion = project.version.replace("-SNAPSHOT", "") + +release { + failOnPublishNeeded = false + failOnSnapshotDependencies = true + + buildTasks = ['build'] + versionPropertyFile = 'gradle.properties' + tagTemplate = 'v$version' + + git { + requireBranch = "release-${moduleVersion}" + pushToRemote = 'origin' + } +} + +task build { + dependsOn(":${packageName}-ballerina:build") +} + +publishToMavenLocal.dependsOn build +publish.dependsOn build diff --git a/docs/spec/spec.md b/docs/spec/spec.md new file mode 100644 index 0000000..3d13494 --- /dev/null +++ b/docs/spec/spec.md @@ -0,0 +1 @@ +# Specification: Ballerina Persist Library \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..afc8e91 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,55 @@ +group=io.ballerina.lib +version=0.1.0-SNAPSHOT + +puppycrawlCheckstyleVersion=10.12.0 +checkstyleToolVersion=10.12.0 +githubSpotbugsVersion=5.0.14 +githubJohnrengelmanShadowVersion=8.1.1 +underCouchDownloadVersion=5.4.0 +researchgateReleaseVersion=2.8.0 +testngVersion=7.6.1 +gsonVersion=2.10 +ballerinaGradlePluginVersion=2.0.1 + +ballerinaLangVersion=2201.9.0-20240229-103900-a949e6d4 + +# Direct Dependencies +# Level 01 +stdlibIoVersion=1.6.0 +stdlibTimeVersion=2.4.0 +stdlibUrlVersion=2.4.0 +stdlibConstraintVersion=1.4.0 + +# Level 02 +stdlibLogVersion=2.9.0 +stdlibOsVersion=1.8.0 +stdlibCryptoVersion=2.5.0 +stdlibTaskVersion=2.5.0 + +# Level 03 +stdlibFileVersion=1.9.0 +stdlibCacheVersion=3.7.0 +stdlibMimeVersion=2.9.0 +stdlibUuidVersion=1.7.0 + +# Level 04 +stdlibAuthVersion=2.10.0 +stdlibJwtVersion=2.10.0 +stdlibOAuth2Version=2.10.0 + +# Level 05 +stdlibHttpVersion=2.10.0 + +# Level 08 +stdlibRedisVersion=3.0.0 + +# Level 09 +stdlibPersistVersion=1.2.0 + +# Ballerinax Observer +observeVersion=1.2.0 +observeInternalVersion=1.2.0 + +# Enabled publishing insecure checksums, due to fail to publish to maven central +# Refer https://github.com/gradle/gradle/issues/11308 +systemProp.org.gradle.internal.publish.checksums.insecure=true 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..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/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##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && 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=SC2039,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=SC2039,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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +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..93e3f59 --- /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/native/build.gradle b/native/build.gradle new file mode 100644 index 0000000..53519b5 --- /dev/null +++ b/native/build.gradle @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +plugins { + id 'java' + id 'com.github.spotbugs' + id 'checkstyle' + id 'jacoco' + id 'maven-publish' +} + +description = 'Ballerina - Persist Java Native' + +configurations { + jacocoRuntime +} + +dependencies { + jacocoRuntime "org.jacoco:org.jacoco.agent:${jacoco.toolVersion}:runtime" + + checkstyle project(":checkstyle") + checkstyle "com.puppycrawl.tools:checkstyle:${puppycrawlCheckstyleVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-runtime', version: "${ballerinaLangVersion}" + implementation group: 'io.ballerina.stdlib', name: 'persist-native', version: "${stdlibPersistVersion}" +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +sourceCompatibility = JavaVersion.VERSION_17 + +jacoco { + toolVersion = "0.8.8" +} + +test { + useTestNG() { + suites 'src/test/resources/testng.xml' + } + testLogging.showStandardStreams = true + testLogging { + events "PASSED", "FAILED", "SKIPPED" + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" + def startItem = '| ', endItem = ' |' + def repeatLength = startItem.length() + output.length() + endItem.length() + println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) + } + } + } + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + } +} + +spotbugsMain { + ignoreFailures = true + effort = "max" + reportLevel = "low" + reportsDir = file("$project.buildDir/reports/spotbugs") + def excludeFile = file("${rootDir}/build-config/spotbugs-exclude.xml") + if (excludeFile.exists()) { + it.excludeFilter = excludeFile + } + reports { + text.enabled = true + } +} + +spotbugsTest { + enabled = false +} + +task validateSpotbugs() { + doLast { + if (spotbugsMain.reports.size() > 0 && + spotbugsMain.reports[0].destination.exists() && + spotbugsMain.reports[0].destination.text.readLines().size() > 0) { + spotbugsMain.reports[0].destination?.eachLine { + println 'Failure: ' + it + } + throw new GradleException("Spotbugs rule violations were found."); + } + } +} + +checkstyle { + toolVersion "${checkstyleToolVersion}" + configFile file("${rootDir}/build-config/checkstyle/build/checkstyle.xml") + configProperties = ["suppressionFile": file("${rootDir}/build-config/checkstyle/build/suppressions.xml")] +} + +tasks.withType(Checkstyle) { + exclude '**/module-info.java' +} + +spotbugsMain.finalizedBy validateSpotbugs +checkstyleMain.dependsOn ':checkstyle:downloadCheckstyleRuleFiles' +checkstyleTest.dependsOn ':checkstyle:downloadCheckstyleRuleFiles' + +publishing { + publications { + mavenJava(MavenPublication) { + groupId project.group + artifactId "persist.redis-native" + version = project.version + artifact jar + } + } + + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/ballerina-platform/module-ballerina-persist") + credentials { + username = System.getenv("publishUser") + password = System.getenv("publishPAT") + } + } + maven { + name = "WSO2Nexus" + if(project.version.endsWith('-SNAPSHOT')) { + url "https://maven.wso2.org/nexus/content/repositories/snapshots/" + } else { + url "https://maven.wso2.org/nexus/service/local/staging/deploy/maven2/" + } + credentials { + username System.getenv("nexusUser") + password System.getenv("nexusPassword") + } + } + } +} + +compileJava { + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/persist/redis/Constants.java b/native/src/main/java/io/ballerina/stdlib/persist/redis/Constants.java new file mode 100644 index 0000000..81cd7e3 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/persist/redis/Constants.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +package io.ballerina.stdlib.persist.redis; + +public class Constants { + private Constants() { + + } + + public static final String PERSIST_REDIS_STREAM = "PersistRedisStream"; +} diff --git a/native/src/main/java/io/ballerina/stdlib/persist/redis/ModuleUtils.java b/native/src/main/java/io/ballerina/stdlib/persist/redis/ModuleUtils.java new file mode 100644 index 0000000..4edb701 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/persist/redis/ModuleUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +package io.ballerina.stdlib.persist.redis; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.Module; + +public class ModuleUtils { + private static Module redisModule; + + private ModuleUtils() { + } + + public static void setModule(Environment env) { + redisModule = env.getCurrentModule(); + } + + public static Module getModule() { + return redisModule; + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/persist/redis/Utils.java b/native/src/main/java/io/ballerina/stdlib/persist/redis/Utils.java new file mode 100644 index 0000000..61c3fa0 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/persist/redis/Utils.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +package io.ballerina.stdlib.persist.redis; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.Field; +import io.ballerina.runtime.api.types.MapType; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BStream; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; + +import java.util.Locale; +import java.util.Map; + +import static io.ballerina.runtime.api.utils.StringUtils.fromString; +import static io.ballerina.stdlib.persist.redis.Constants.PERSIST_REDIS_STREAM; +import static io.ballerina.stdlib.persist.redis.ModuleUtils.getModule; + +public class Utils { + + public static BString getEntityFromStreamMethod(Environment env) { + String functionName = env.getFunctionName(); + String entity = functionName.substring(5, functionName.length() - 6).toLowerCase(Locale.ENGLISH); + return fromString(entity); + } + + public static BMap getFieldTypes(RecordType recordType) { + MapType stringMapType = TypeCreator.createMapType(PredefinedTypes.TYPE_STRING); + BMap typeMap = ValueCreator.createMapValue(stringMapType); + Map fieldsMap = recordType.getFields(); + for (Field field : fieldsMap.values()) { + + Type type = field.getFieldType(); + String fieldName = field.getFieldName(); + typeMap.put(StringUtils.fromString(fieldName), StringUtils.fromString(type.getName())); + } + return typeMap; + } + + private static BObject createPersistRedisStream(BStream redisStream, + BTypedesc targetType, BMap typeMap, BArray fields, + BArray includes, BArray typeDescriptions, BObject persistClient, + BError persistError) { + return ValueCreator.createObjectValue(getModule(), PERSIST_REDIS_STREAM, + redisStream, targetType, typeMap, fields, includes, typeDescriptions, persistClient, persistError); + } + + private static BStream createPersistRedisStreamValue(BTypedesc targetType, BObject persistRedisStream) { + RecordType streamConstraint = (RecordType) TypeUtils.getReferredType(targetType.getDescribingType()); + return ValueCreator.createStreamValue( + TypeCreator.createStreamType(streamConstraint, PredefinedTypes.TYPE_NULL), persistRedisStream); + } + + public static BStream createPersistRedisStreamValue(BStream redisStream, BTypedesc targetType, BArray fields, + BArray includes, BArray typeDescriptions, BObject persistClient, + BError persistError) { + BObject persistRedisStream = createPersistRedisStream(redisStream, targetType, + Utils.getFieldTypes((RecordType) targetType.getDescribingType()), fields, includes, + typeDescriptions, persistClient, persistError); + return createPersistRedisStreamValue(targetType, persistRedisStream); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/persist/redis/datastore/RedisProcessor.java b/native/src/main/java/io/ballerina/stdlib/persist/redis/datastore/RedisProcessor.java new file mode 100644 index 0000000..9c3e3ee --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/persist/redis/datastore/RedisProcessor.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +package io.ballerina.stdlib.persist.redis.datastore; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.Future; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.async.Callback; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.ErrorType; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.types.StreamType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BStream; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; +import io.ballerina.stdlib.persist.Constants; +import io.ballerina.stdlib.persist.ModuleUtils; +import io.ballerina.stdlib.persist.redis.Utils; + +import java.util.Map; + +import static io.ballerina.stdlib.persist.Constants.ERROR; +import static io.ballerina.stdlib.persist.Constants.KEY_FIELDS; +import static io.ballerina.stdlib.persist.Constants.RUN_READ_BY_KEY_QUERY_METHOD; +import static io.ballerina.stdlib.persist.ErrorGenerator.wrapError; +import static io.ballerina.stdlib.persist.Utils.getEntity; +import static io.ballerina.stdlib.persist.Utils.getKey; +import static io.ballerina.stdlib.persist.Utils.getMetadata; +import static io.ballerina.stdlib.persist.Utils.getPersistClient; +import static io.ballerina.stdlib.persist.Utils.getRecordTypeWithKeyFields; +import static io.ballerina.stdlib.persist.Utils.getTransactionContextProperties; +import static io.ballerina.stdlib.persist.redis.Utils.getFieldTypes; + +public class RedisProcessor { + + private RedisProcessor() { + + } + + public static BStream query(Environment env, BObject client, BTypedesc targetType) { + // This method will return `stream` + + BString entity = getEntity(env); + BObject persistClient = getPersistClient(client, entity); + BArray keyFields = (BArray) persistClient.get(KEY_FIELDS); + RecordType recordType = (RecordType) targetType.getDescribingType(); + + RecordType recordTypeWithIdFields = getRecordTypeWithKeyFields(keyFields, recordType); + BTypedesc targetTypeWithIdFields = ValueCreator.createTypedescValue(recordTypeWithIdFields); + StreamType streamTypeWithIdFields = TypeCreator.createStreamType(recordTypeWithIdFields, + PredefinedTypes.TYPE_NULL); + + Map trxContextProperties = getTransactionContextProperties(); + String strandName = env.getStrandName().isPresent() ? env.getStrandName().get() : null; + + BArray[] metadata = getMetadata(recordType); + BArray fields = metadata[0]; + BArray includes = metadata[1]; + BArray typeDescriptions = metadata[2]; + BMap typeMap = getFieldTypes(recordType); + + Future balFuture = env.markAsync(); + env.getRuntime().invokeMethodAsyncSequentially( + // Call `RedisClient.runReadQuery( + // typedesc rowType, map typeMap, string[] fields = [], + // string[] include = [] + // )` + // which returns `stream|persist:Error` + + persistClient, Constants.RUN_READ_QUERY_METHOD, strandName, env.getStrandMetadata(), new Callback() { + @Override + public void notifySuccess(Object o) { + if (o instanceof BStream) { // stream + BStream redisStream = (BStream) o; + balFuture.complete(Utils.createPersistRedisStreamValue(redisStream, targetType, fields, + includes, typeDescriptions, persistClient, null)); + } else { // persist:Error + balFuture.complete(Utils.createPersistRedisStreamValue(null, targetType, fields, includes, + typeDescriptions, persistClient, (BError) o)); + } + } + + @Override + public void notifyFailure(BError bError) { + balFuture.complete(Utils.createPersistRedisStreamValue(null, targetType, fields, includes, + typeDescriptions, persistClient, wrapError(bError))); + } + }, trxContextProperties, streamTypeWithIdFields, + targetTypeWithIdFields, true, typeMap, true, fields, true, includes, true); + + return null; + } + + public static Object queryOne(Environment env, BObject client, BArray path, BTypedesc targetType) { + // This method will return `targetType|persist:Error` + + BString entity = getEntity(env); + BObject persistClient = getPersistClient(client, entity); + BArray keyFields = (BArray) persistClient.get(KEY_FIELDS); + RecordType recordType = (RecordType) targetType.getDescribingType(); + + RecordType recordTypeWithIdFields = getRecordTypeWithKeyFields(keyFields, recordType); + ErrorType persistErrorType = TypeCreator.createErrorType(ERROR, ModuleUtils.getModule()); + Type unionType = TypeCreator.createUnionType(recordTypeWithIdFields, persistErrorType); + + BArray[] metadata = getMetadata(recordType); + BArray fields = metadata[0]; + BArray includes = metadata[1]; + BArray typeDescriptions = metadata[2]; + BMap typeMap = getFieldTypes(recordType); + + Object key = getKey(env, path); + + Map trxContextProperties = getTransactionContextProperties(); + String strandName = env.getStrandName().isPresent() ? env.getStrandName().get() : null; + + Future balFuture = env.markAsync(); + env.getRuntime().invokeMethodAsyncSequentially( + // Call `RedisClient.runReadByKeyQuery( + // typedesc rowType, anydata key, string[] fields = [], string[] + // include = [], + // typedesc[] typeDescriptions = [] + // )` + // which returns `record {}|persist:Error` + + persistClient, RUN_READ_BY_KEY_QUERY_METHOD, strandName, env.getStrandMetadata(), + new Callback() { + @Override + public void notifySuccess(Object o) { + balFuture.complete(o); + } + + @Override + public void notifyFailure(BError bError) { + balFuture.complete(wrapError(bError)); + } + }, trxContextProperties, unionType, + targetType, true, typeMap, true, key, true, fields, true, includes, true, + typeDescriptions, true); + + return null; + } +} diff --git a/native/src/main/java/module-info.java b/native/src/main/java/module-info.java new file mode 100644 index 0000000..759b38d --- /dev/null +++ b/native/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) 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. + */ + +module io.ballerina.stdlib.persist.redis { + requires io.ballerina.runtime; + requires io.ballerina.lang; + requires io.ballerina.stdlib.persist; +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..245de3f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,47 @@ +/* + * 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.5/userguide/building_swift_projects.html in the Gradle documentation. + */ + +pluginManagement { + plugins { + id "com.github.spotbugs" version "${githubSpotbugsVersion}" + id "com.github.johnrengelman.shadow" version "${githubJohnrengelmanShadowVersion}" + id "de.undercouch.download" version "${underCouchDownloadVersion}" + id "net.researchgate.release" version "${researchgateReleaseVersion}" + } + + 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.13.2" +} + +rootProject.name = 'ballerinax-persist.redis' + +include ':checkstyle' +include ':persist.redis-native' +include ':persist.redis-ballerina' + +project(':checkstyle').projectDir = file("build-config${File.separator}checkstyle") +project(':persist.redis-native').projectDir = file('native') +project(':persist.redis-ballerina').projectDir = file('ballerina') + +gradleEnterprise { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +}