Merge branch 'appbuilder/sprint-13' into gh-11817-listview-margin

This commit is contained in:
Nakul Nagargade 2025-06-05 14:40:14 +05:30
commit 6b6375d85c
118 changed files with 8210 additions and 731 deletions

View file

@ -12,91 +12,106 @@ env:
jobs:
Cypress-Platform:
runs-on: ubuntu-22.04
if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce')
if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce')
strategy:
fail-fast: false
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') && fromJson('["ee"]') ||
fromJson('[]')
}}
edition:
- ${{ contains(github.event.pull_request.labels.*.name, 'run-cypress') && 'ce' || contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') && 'ce' || contains(github.event.pull_request.labels.*.name, 'run-cypress-ce') && 'ce' || '' }}
- ${{ contains(github.event.pull_request.labels.*.name, 'run-cypress') && 'ee' || contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') && 'ee' || '' }}
exclude:
- edition: ""
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.18.2
- name: Set up Git authentication for private submodules
- name: Debug labels and matrix edition
run: |
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
echo "Labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}"
echo "Matrix edition: ${{ matrix.edition }}"
- name: Checkout with Submodules
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Checking out the correct branch for submodules EE
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Docker Login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set SAFE_BRANCH_NAME
run: echo "SAFE_BRANCH_NAME=$(echo ${{ env.BRANCH_NAME }} | tr '/' '-')" >> $GITHUB_ENV
- name: Build CE Docker image
if: matrix.edition == 'ce'
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build EE Docker image
if: matrix.edition == 'ee'
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
- name: Install and build dependencies
run: |
npm cache clean --force
npm install
npm install --prefix server
npm install --prefix frontend
npm run build:plugins
- name: Local development setup
run: |
sudo docker network create tooljet
sudo docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_PORT=5432 -d postgres:13
- name: Run PostgREST Docker Container
run: |
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
-e PGRST_DB_URI="postgres://postgres:postgres@localhost:5432/tooljet" \
-e PGRST_DB_ANON_ROLE="postgres" \
-e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
-e PGRST_DB_PRE_CONFIG=postgrest.pre_config \
postgrest/postgrest:v12.2.0
uses: docker/build-push-action@v4
with:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.pull_request.head.ref }}
file: cypress-tests/cypress.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'ee' || 'ce' }}" >> .env
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "TOOLJET_EDITION=${{ matrix.edition }}" >> .env
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=localhost" >> .env
echo "PG_HOST=postgres" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet_db" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=localhost" >> .env
echo "TOOLJET_DB_HOST=postgres" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "TOOLJET_DB_STATEMENT_TIMEOUT=60000" >> .env
echo "TOOLJET_DB_RECONFIG=true" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=localhost:3001" >> .env
echo "PGRST_DB_PRE_CONFIG=postgrest.pre_config" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@localhost:5432/tooljet" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
echo "ENABLE_MARKETPLACE_FEATURE=true" >> .env
echo "ENABLE_MARKETPLACE_DEV_MODE=true" >> .env
echo "ENABLE_PRIVATE_APP_EMBED=true" >> .env
@ -105,29 +120,50 @@ jobs:
echo "SSO_GIT_OAUTH2_CLIENT_ID=1234567890" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=3346shfvkdjjsfkvxce32854e026a4531ed" >> .env
- name: Set up database
run: |
npm run --prefix server db:create
npm run --prefix server db:reset
sleep 5
# Only add EE-specific env vars if edition is ee
if [ "${{ matrix.edition }}" = "ee" ]; then
echo "SSO_OPENID_NAME=tj-oidc-simulator" >> .env
echo "SSO_OPENID_CLIENT_ID=${{ secrets.SSO_OPENID_CLIENT_ID }}" >> .env
echo "SSO_OPENID_CLIENT_SECRET=${{ secrets.SSO_OPENID_CLIENT_SECRET }}" >> .env
echo "SSO_OPENID_WELL_KNOWN_URL=http://34.66.166.236:8080/.well-known/openid-configuration" >> .env
echo "LICENSE_KEY=${{ secrets.RENDER_LICENSE_KEY }}" >> .env
fi
- name: Start services
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
- name: Update docker-compose file
run: |
cd plugins && npm start &
cd server && npm run start:dev &
cd frontend && npm start &
# Update docker-compose.yaml with the appropriate image based on edition
if [ "${{ matrix.edition }}" = "ce" ]; then
sed -i '/^[[:space:]]*tooljet:/,/^$/ s|^\([[:space:]]*image:[[:space:]]*\).*|\1tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce|' docker-compose.yaml
elif [ "${{ matrix.edition }}" = "ee" ]; then
sed -i '/^[[:space:]]*tooljet:/,/^$/ s|^\([[:space:]]*image:[[:space:]]*\).*|\1tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee|' docker-compose.yaml
fi
- name: Install Docker Compose
run: |
curl -L "https://github.com/docker/compose/releases/download/v2.10.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
- name: Run docker-compose file
run: docker-compose up -d
- name: Checking containers
run: docker ps -a
- name: docker logs
run: sudo docker logs Tooljet-app
- name: Wait for the server to be ready
run: |
timeout 300 bash -c '
until curl --silent --fail http://localhost:8082; do
timeout 500 bash -c '
until curl --silent --fail http://localhost:3000; do
sleep 5
done'
- name: Postgres logs
run: docker logs postgrest
- name: Create Cypress environment file
- name: Create Cypress environment file for CE
if: matrix.edition == 'ce'
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
@ -135,13 +171,30 @@ jobs:
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Run Cypress tests
- name: Run Cypress tests for CE
if: matrix.edition == 'ce'
uses: cypress-io/github-action@v6
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:8082"
config: "baseUrl=http://localhost:3000"
config-file: cypress-platform.config.js
- name: Create Cypress environment file for EE
if: matrix.edition == 'ee'
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_EE_SECRETS }}
dir: "./cypress-tests"
- name: Run Cypress tests for EE
if: matrix.edition == 'ee'
uses: cypress-io/github-action@v6
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:3000"
config-file: cypress-ee-platform.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v4
if: always()

View file

@ -10,14 +10,12 @@ env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
deploy-marketplace-plugin:
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'deploy-marketplace-plugin' }}
runs-on: ubuntu-latest
steps:
- name: Sync repo
uses: actions/checkout@v3
@ -49,28 +47,58 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_SECRET_MAR_ACCESS_KEY }}
aws-region: us-east-2
- name: Install and build dependencies
- name: Install and build dependencies in order
run: |
cd marketplace && npm install && npm run build --workspaces
continue-on-error: true
- name: Build marketplace plugins
run: |
cd marketplace && AWS_BUCKET=tooljet-plugins-stage node scripts/upload-to-s3.js
cd marketplace
echo "🔧 Installing all workspace dependencies"
npm install
- name: Comment deployment URL
echo "🏗️ Building 'common' plugin first"
npm run build --workspace=plugins/common || exit 1
echo "🔁 Building all remaining plugins"
PLUGINS=$(ls plugins | grep -v '^common$')
for plugin in $PLUGINS; do
echo "🔨 Building plugin: $plugin"
npm run build --workspace=plugins/$plugin || exit 1
done
- name: Build marketplace plugins and capture summary
run: |
cd marketplace
echo "🚀 Uploading to S3"
AWS_BUCKET=tooljet-plugins-stage node scripts/upload-to-s3.js | tee upload_summary.log
- name: Extract upload summary
id: upload_summary
run: |
SUMMARY=$(awk '/UPLOAD SUMMARY/,0' marketplace/upload_summary.log)
echo "UPLOAD_SUMMARY<<EOF" >> $GITHUB_ENV
echo "$SUMMARY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Comment on success
if: success()
uses: actions/github-script@v5
with:
github-token: ${{secrets.GITHUB_TOKEN}}
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const runId = process.env.GITHUB_RUN_ID;
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${runId}`;
const summary = process.env.UPLOAD_SUMMARY;
const body = `Marketplace Plugin added to stage bucket\n\n🔍 [View Deployment Logs & Summary](${runUrl})\n\n\`\`\`\n${summary}\n\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Marketplace Plugin added to stage bucket'
})
body
});
- uses: actions/github-script@v6
- name: Label update on success
if: success()
uses: actions/github-script@v6
with:
script: |
try {
@ -90,3 +118,19 @@ jobs:
repo: context.repo.repo,
labels: ['marketplace-plugin-deployed']
})
- name: Comment on failure
if: failure()
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const runId = process.env.GITHUB_RUN_ID;
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${runId}`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `❌ Marketplace Plugin deployment failed.\n\n🔍 [View Deployment Logs & Summary](${runUrl})`
});

View file

@ -1,7 +1,9 @@
name: Render review deploy
on:
pull_request_target:
types: [labeled, unlabeled, closed]
types: [labeled, unlabeled, closed, synchronize, opened]
issue_comment:
types: [created, edited, deleted]
env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
@ -1153,3 +1155,234 @@ jobs:
# } catch (e) {
# console.log(e)
# }
redeploy-review-app:
if: ${{ github.event.action == 'synchronize' || github.event.action == 'opened' }}
runs-on: ubuntu-latest
steps:
- name: Get PR labels
id: get_labels
uses: actions/github-script@v6
with:
script: |
const labels = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
return labels.data.map(l => l.name);
- name: Redeploy CE review app if active
if: contains(steps.get_labels.outputs.result, 'active-ce-review-app')
id: redeploy_ce
env:
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
PR_NUMBER: ${{ github.event.number }}
run: |
SERVICE_ID=$(curl --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" | jq -r '.[0].service.id')
DEPLOY_RESPONSE=$(curl --request POST \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys" \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" \
--data '{"clearCache":"clear"}')
DEPLOY_ID=$(echo $DEPLOY_RESPONSE | jq -r '.id')
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
- name: Redeploy EE review app if active
if: contains(steps.get_labels.outputs.result, 'active-ee-review-app')
id: redeploy_ee
env:
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
PR_NUMBER: ${{ github.event.number }}
run: |
SERVICE_ID=$(curl --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" | jq -r '.[0].service.id')
DEPLOY_RESPONSE=$(curl --request POST \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys" \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" \
--data '{"clearCache":"clear"}')
DEPLOY_ID=$(echo $DEPLOY_RESPONSE | jq -r '.id')
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
render-bot-check-deployment:
runs-on: ubuntu-latest
if: github.event.action == 'labeled' && github.event.label.name == 'render-check-deployment'
steps:
- name: Get PR labels
id: get_labels
uses: actions/github-script@v6
with:
script: |
const labels = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
return labels.data.map(l => l.name);
- name: Fetch CE service and deploy ID
run: |
response=$(curl --silent --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
SERVICE_ID=$(echo "$response" | jq -r 'if type=="array" and length > 0 then .[0].service.id else empty end')
if [[ -z "$SERVICE_ID" ]]; then
echo "No CE service found for PR #$PR_NUMBER. Skipping deployment ID fetch."
exit 0
fi
response_deploy=$(curl --silent --request GET \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys?limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
DEPLOY_ID=$(echo "$response_deploy" | jq -r 'if type=="array" and length > 0 then .[0].deploy.id else empty end')
echo "CE_SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "CE_DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
- name: Comment CE deployment details
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = process.env.PR_NUMBER;
const apiKey = process.env.RENDER_API_KEY;
const ceServiceRes = await fetch(`https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${prNumber}&limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const ceServices = await ceServiceRes.json();
const ceServiceId = ceServices[0]?.service?.id || null;
let ceInfo = 'No active CE review app deployment found.';
if (ceServiceId) {
const deployRes = await fetch(`https://api.render.com/v1/services/${ceServiceId}/deploys?limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const deployData = await deployRes.json();
const deploy = deployData[0]?.deploy || {};
const ceCommit = deploy.commit || {};
const status = deploy.status || 'unknown';
ceInfo = `### Community Edition\n- App: https://tooljet-ce-pr-${prNumber}.onrender.com\n- Dashboard: https://dashboard.render.com/web/${ceServiceId}\n- Commit: ${ceCommit.id || ''}\n- Message: ${ceCommit.message || ''}\n- Status: ${status}`;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: ceInfo
});
- name: Fetch EE service and deploy ID
run: |
response=$(curl --silent --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
SERVICE_ID=$(echo "$response" | jq -r 'if type=="array" and length > 0 then .[0].service.id else empty end')
if [[ -z "$SERVICE_ID" ]]; then
echo "No EE service found for PR #$PR_NUMBER. Skipping deployment ID fetch."
exit 0
fi
response_deploy=$(curl --silent --request GET \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys?limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
DEPLOY_ID=$(echo "$response_deploy" | jq -r 'if type=="array" and length > 0 then .[0].deploy.id else empty end')
echo "EE_SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "EE_DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
- name: Comment EE deployment details
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = process.env.PR_NUMBER;
const apiKey = process.env.RENDER_API_KEY;
const eeServiceRes = await fetch(`https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${prNumber}&limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const eeServices = await eeServiceRes.json();
const eeServiceId = eeServices[0]?.service?.id || null;
let eeInfo = 'No active EE review app deployment found.';
if (eeServiceId) {
const deployRes = await fetch(`https://api.render.com/v1/services/${eeServiceId}/deploys?limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const deployData = await deployRes.json();
const deploy = deployData[0]?.deploy || {};
const eeCommit = deploy.commit || {};
const status = deploy.status || 'unknown';
eeInfo = `### Enterprise Edition\n- App: https://tooljet-ee-pr-${prNumber}.onrender.com\n- Dashboard: https://dashboard.render.com/web/${eeServiceId}\n- Commit: ${eeCommit.id || ''}\n- Message: ${eeCommit.message || ''}\n- Status: ${status}`;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: eeInfo
});
- name: Remove label
if: contains(steps.get_labels.outputs.result, 'render-check-deployment')
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'render-check-deployment'
})
} catch (e) {
console.log(e)
}

View file

@ -1 +1 @@
3.13.0
3.14.0

View file

@ -0,0 +1,114 @@
const { defineConfig } = require("cypress");
const { rmdir } = require("fs");
const fs = require("fs");
const XLSX = require("node-xlsx");
const pg = require("pg");
const path = require("path");
const pdf = require("pdf-parse");
const environments = {
'run-cypress-platform': {
baseUrl: "http://localhost:3000",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-subpath': {
baseUrl: "http://localhost:3000/apps",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-proxy': {
baseUrl: "http://localhost:4001",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-proxy-subpath': {
baseUrl: "http://localhost:4001/apps",
configFile: "cypress-platform.config.js"
}
};
const githubLabel = process.env.GITHUB_LABEL || 'run-cypress-platform';
const environment = environments[githubLabel];
module.exports = defineConfig({
execTimeout: 1800000,
defaultCommandTimeout: 30000,
requestTimeout: 30000,
pageLoadTimeout: 30000,
responseTimeout: 30000,
viewportWidth: 1440,
viewportHeight: 960,
chromeWebSecurity: false,
trashAssetsBeforeRuns: true,
e2e: {
setupNodeEvents (on, config) {
config.baseUrl = environment.baseUrl;
on("task", {
readPdf (pathToPdf) {
return new Promise((resolve) => {
const pdfPath = path.resolve(pathToPdf);
let dataBuffer = fs.readFileSync(pdfPath);
pdf(dataBuffer).then(function ({ text }) {
resolve(text);
});
});
},
});
on("task", {
readXlsx (filePath) {
return new Promise((resolve, reject) => {
try {
let dataBuffer = fs.readFileSync(filePath);
const jsonData = XLSX.parse(dataBuffer);
resolve(jsonData[0]["data"].toString());
} catch (e) {
reject(e);
}
});
},
});
on("task", {
deleteFolder (folderName) {
return new Promise((resolve, reject) => {
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
if (err) {
console.error(err);
return reject(err);
}
resolve(null);
});
});
},
});
on("task", {
dbConnection ({ dbconfig, sql }) {
const client = new pg.Pool(dbconfig);
return client.query(sql);
},
});
return require("./cypress/plugins/index.js")(on, config);
},
downloadsFolder: "cypress/downloads",
experimentalRunAllSpecs: true,
experimentalModfyObstructiveThirdPartyCode: true,
baseUrl: environment.baseUrl,
configFile: environment.configFile,
specPattern: [
"cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js",
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
"cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js",
],
numTestsKeptInMemory: 1,
redirectionLimit: 15,
experimentalMemoryManagement: true,
video: false,
videoUploadOnPasses: false,
retries: {
runMode: 2,
openMode: 0,
},
},
});

View file

@ -8,7 +8,7 @@ const pdf = require("pdf-parse");
const environments = {
'run-cypress-platform': {
baseUrl: "http://localhost:8082",
baseUrl: "http://localhost:3000",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-subpath': {

View file

@ -0,0 +1,189 @@
FROM node:18.18.2-buster AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN mkdir -p /app
WORKDIR /app
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME
# Clone and checkout the frontend repositorys
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
RUN git config --global http.version HTTP/1.1
RUN git config --global http.postBuffer 524288000
RUN git clone https://github.com/ToolJet/ToolJet.git .
# The branch name needs to be changed the branch with modularisation in CE repo
RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
RUN git submodule foreach 'git checkout ${BRANCH_NAME} || true'
# Scripts for building
COPY ./package.json ./package.json
# Build plugins
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
RUN npm --prefix plugins install
COPY ./plugins/ ./plugins/
RUN NODE_ENV=production npm --prefix plugins run build
RUN npm --prefix plugins prune --production
ENV TOOLJET_EDITION=ee
# Build frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN npm --prefix frontend install
COPY ./frontend/ ./frontend/
RUN npm --prefix frontend run build --production
RUN npm --prefix frontend prune --production
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
# Build server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm --prefix server run build
FROM node:18.18.2-bullseye
RUN apt-get update -yq \
&& apt-get install curl wget gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt -y install redis \
&& apt-get clean -y
# copy postgrest executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt-get update && apt-get install -y freetds-dev libaio1 wget supervisor
# Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle
RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linuxx64.zip && \
wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \
unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \
unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \
cd /opt/oracle/instantclient_21_10 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
cd /opt/oracle/instantclient_11_2 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
# Set the Instant Client library paths
ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}"
WORKDIR /
# copy npm scripts
COPY --from=builder /app/package.json ./app/package.json
# copy plugins dependencies
COPY --from=builder /app/plugins/dist ./app/plugins/dist
COPY --from=builder /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy frontend build
COPY --from=builder /app/frontend/build ./app/frontend/build
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
WORKDIR /app
# Install PostgreSQL
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor --fix-missing
# Explicitly create PG main directory with correct ownership
RUN mkdir -p /var/lib/postgresql/13/main && \
chown -R postgres:postgres /var/lib/postgresql
RUN mkdir -p /var/log/supervisor /var/run/postgresql && \
chown -R postgres:postgres /var/run/postgresql /var/log/supervisor
# Remove existing data and create directory with proper ownership
RUN rm -rf /var/lib/postgresql/13/main && \
mkdir -p /var/lib/postgresql/13/main && \
chown -R postgres:postgres /var/lib/postgresql
# Initialize PostgreSQL
RUN su - postgres -c "/usr/lib/postgresql/13/bin/initdb -D /var/lib/postgresql/13/main"
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
"user=root \n" \
"\n" \
"[program:redis] \n" \
"command=redis-server /etc/redis/redis.conf \n" \
"user=redis \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/var/log/redis/redis-server.log \n" \
"stdout_logfile=/var/log/redis/redis-server.log \n" \
"\n" \
"[program:postgrest] \n" \
"command=/bin/postgrest \n" \
"autostart=true \n" \
"autorestart=true \n" \
"\n" \
"[program:tooljet] \n" \
"user=root \n" \
"command=/bin/bash -c '/app/server/scripts/boot.sh' \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
ENV TOOLJET_HOST=http://localhost \
PORT=3000 \
NODE_ENV=production \
LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \
SECRET_KEY_BASE=replace_with_secret_key_base \
PG_DB=tooljet_production \
PG_USER=postgres \
PG_PASS=postgres \
PG_HOST=localhost \
ENABLE_TOOLJET_DB=true \
TOOLJET_DB_HOST=localhost \
TOOLJET_DB_USER=postgres \
TOOLJET_DB_PASS=postgres \
TOOLJET_DB=tooljet_db \
PGRST_HOST=http://localhost:3001 \
PGRST_SERVER_PORT=3001 \
PGRST_DB_URI=postgres://postgres:postgres@localhost/tooljet_db \
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_USER= \
REDIS_PASSWORD= \
ORM_LOGGING=true \
DEPLOYMENT_PLATFORM=docker:local \
HOME=/home/appuser \
TERM=xterm
RUN chmod +x ./server/scripts/preview.sh
# Set the entrypoint
ENTRYPOINT ["./server/scripts/preview.sh"]

View file

@ -52,7 +52,7 @@ Cypress.Commands.add("apiCreateGDS", (url, name, kind, options) => {
log: false;
}
expect(response.status).to.equal(201);
Cypress.env(`${kind}`, response.body.id);
Cypress.env(`${name}`, response.body.id);
Cypress.log({
name: "Create Data Source",
@ -80,13 +80,14 @@ Cypress.Commands.add("apiFetchDataSourcesId", () => {
Cypress.log({
name: "DS Fetch",
displayName: "Data Sources Fetched",
message: dataSources.map(ds => `\nKind: '${ds.kind}', Name: '${ds.id}'`).join(','),
message: dataSources
.map((ds) => `\nKind: '${ds.kind}', Name: '${ds.id}'`)
.join(","),
});
});
});
});
Cypress.Commands.add("apiCreateApp", (appName = "testApp") => {
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("walkthroughCompleted", "true");
@ -168,7 +169,6 @@ Cypress.Commands.add(
Cypress.env("editingVersionId", responseData.editing_version.id);
Cypress.env("environmentId", responseData.editorEnvironment.id);
});
cy.get(componentSelector, { timeout: 10000 });
}
@ -221,21 +221,21 @@ Cypress.Commands.add(
const requestBody =
envVar === "Enterprise"
? {
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
}
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
}
: {
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
};
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
};
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
@ -289,7 +289,9 @@ Cypress.Commands.add("apiAddQuery", (queryName, query, dataQueryId) => {
Cypress.Commands.add(
"apiAddQueryToApp",
(queryName, options, dsName, dsKind) => {
cy.log(`${Cypress.env("server_host")}/api/data-queries/data-sources/${Cypress.env(dsKind)}/versions/${Cypress.env("editingVersionId")}`)
cy.log(
`${Cypress.env("server_host")}/api/data-queries/data-sources/${Cypress.env(dsKind)}/versions/${Cypress.env("editingVersionId")}`
);
cy.getCookie("tj_auth_token", { log: false }).then((cookie) => {
const authToken = `tj_auth_token=${cookie.value}`;
const workspaceId = Cypress.env("workspaceId");
@ -737,3 +739,55 @@ Cypress.Commands.add("apiGetAppData", (appId = Cypress.env("appId")) => {
});
});
});
Cypress.Commands.add("apiDeleteGDS", (name) => {
const dataSourceId = Cypress.env(`${name}`);
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/data-sources/${dataSourceId}`,
headers: {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
},
failOnStatusCode: false,
}).then((response) => {
console.log("Delete response:", response);
expect(response.status, "Delete status code").to.eq(200);
Cypress.log({
name: "Delete Data Source",
displayName: "Data source deleted",
message: `Name: '${name}' | ID: '${dataSourceId}'`,
});
});
});
});
Cypress.Commands.add(
"apiUpdateGDS",
({ name, options, envName = "development" }) => {
cy.getAuthHeaders().then((headers) => {
cy.apiGetEnvironments().then((environments) => {
const environment = environments.find((env) => env.name === envName);
const environmentId = environment.id;
const dataSourceId = Cypress.env(`${name}`);
cy.request({
method: "PUT",
url: `${Cypress.env("server_host")}/api/data-sources/${dataSourceId}?environment_id=${environmentId}`,
headers: headers,
body: {
name: name,
options: options,
},
}).then((response) => {
expect(response.status).to.equal(200);
cy.log(`Datasource "${name}" updated successfully.`);
});
});
});
}
);

View file

@ -226,9 +226,9 @@ Cypress.Commands.add(
.invoke("text")
.then((text) => {
cy.wrap(subject).realType(createBackspaceText(text)),
{
delay: 0,
};
{
delay: 0,
};
});
}
);
@ -429,7 +429,6 @@ Cypress.Commands.add("visitSlug", ({ actualUrl }) => {
});
});
Cypress.Commands.add("releaseApp", () => {
if (Cypress.env("environment") !== "Community") {
cy.get(commonEeSelectors.promoteButton).click();
@ -549,7 +548,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
}
});
function installPlugin (pluginName) {
function installPlugin(pluginName) {
cy.get('[data-cy="-list-item"]').eq(1).click();
cy.wait(1000);
@ -605,3 +604,20 @@ Cypress.Commands.add("uninstallMarketplacePlugin", (pluginName) => {
});
});
});
Cypress.Commands.add(
"verifyRequiredFieldValidation",
(fieldName, expectedColor) => {
cy.get(commonSelectors.textField(fieldName)).should(
"have.css",
"border-color",
expectedColor
);
cy.get(commonSelectors.labelFieldValidation(fieldName))
.should("be.visible")
.and("have.text", `${fieldName} is required`);
cy.get(commonSelectors.labelFieldAlert(fieldName))
.should("be.visible")
.and("have.text", `${fieldName} is required`);
}
);

View file

@ -1,5 +1,5 @@
export const cyParamName = (paramName = "") => {
return paramName.toLowerCase().replace(/\s+/g, "-");
return String(paramName).toLowerCase().replace(/\s+/g, "-");
};
export const commonSelectors = {
@ -278,6 +278,16 @@ export const commonSelectors = {
defaultModalTitle: '[data-cy="modal-title"]',
workspaceConstantsIcon: '[data-cy="icon-workspace-constants"]',
confirmationButton: '[data-cy="confirmation-button"]',
textField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-text-field"]`;
},
labelFieldValidation: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-is-required-validation-label"]`;
},
labelFieldAlert: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-is-required-field-alert-text"]`;
},
};
export const commonWidgetSelector = {

View file

@ -28,7 +28,7 @@ export const dataSourceSelector = {
buttonTestConnection: '[data-cy="test-connection-button"]',
connectionFailedText: '[data-cy="test-connection-failed-text"]',
buttonSave: '[data-cy="db-connection-save-button"] > .tj-base-btn',
dangerAlertNotSupportSSL: '.go3958317564',
dangerAlertNotSupportSSL: ".go3958317564",
passwordTextField: '[data-cy="password-text-field"]',
textConnectionVerified: '[data-cy="test-connection-verified-text"]',
@ -101,7 +101,48 @@ export const dataSourceSelector = {
unSavedModalTitle: '[data-cy="unsaved-changes-title"]',
eventQuerySelectionField: '[data-cy="query-selection-field"]',
connectionAlertText: '[data-cy="connection-alert-text"]',
requiredIndicator: '[data-cy="required-indicator"]',
informationIcon: '[data-cy="information-icon"]',
deleteDSButton: (datasourceName) => {
return `[data-cy="${cyParamName(datasourceName)}-delete-button"]`
return `[data-cy="${cyParamName(datasourceName)}-delete-button"]`;
},
labelFieldName: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-field-label"]`;
},
dataSourceNameButton: (dataSourceName) => {
return `[data-cy="${cyParamName(dataSourceName)}-button"]`;
},
dropdownLabel: (label) => {
return `[data-cy="${cyParamName(label)}-dropdown-label"]`;
},
textField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-text-field"]`;
},
subSection: (header) => {
return `[data-cy="${cyParamName(header)}-section"]`;
},
toggleInput: (toggleName) => {
return `[data-cy="${cyParamName(toggleName)}-toggle-input"]`;
},
button: (buttonName) => {
return `[data-cy="button-${cyParamName(buttonName)}"]`;
},
keyInputField: (header, index) => {
return `[data-cy="${cyParamName(header)}-key-input-field-${cyParamName(index)}"]`;
},
valueInputField: (header, index) => {
return `[data-cy="${cyParamName(header)}-value-input-field-${cyParamName(index)}"]`;
},
deleteButton: (header, index) => {
return `[data-cy="${cyParamName(header)}-delete-button-${cyParamName(index)}"]`;
},
addMoreButton: (header) => {
return `[data-cy="${cyParamName(header)}-add-button"]`;
},
dropdownField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-select-dropdown"]`;
},
labelFieldValidation: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-is-required-validation-label"]`;
},
};

View file

@ -87,6 +87,8 @@ export const postgreSqlSelector = {
recordsInputField: '[data-cy="records-input-field"]',
eventQuerySelectionField: '[data-cy="query-selection-field"]',
sslToggleInput: '[data-cy="ssl-enabled-toggle-input"]',
labelEncryptedText: '[data-cy="encrypted-text"]',
};
export const airTableSelector = {

View file

@ -1,6 +1,7 @@
export const airtableText = {
airtable: "Airtable",
cypressairtable: "cypress-Airtable",
ApiKey: "Personal access token",
apikeyPlaceholder: "**************",
};
airtable: "Airtable",
cypressairtable: "cypress-Airtable",
ApiKey: "Personal access token",
apikeyPlaceholder: "**************",
invalidAccessToken: "Authentication failed: Invalid personal access token",
};

View file

@ -17,13 +17,17 @@ export const postgreSqlText = {
allCloudStorage: "Cloud Storages (4)",
postgreSQL: "PostgreSQL",
labelConnectionType: "Connection type",
manualConnectionOption: "Manual connection",
connectionStringOption: "Connection string",
labelHost: "Host",
labelPort: "Port",
labelSSL: "SSL",
labelDbName: "Database name",
labelUserName: "Username",
labelPassword: "Password",
label: "Encrypted",
labelEncrypted: "Encrypted",
labelConnectionOptions: "Connection options",
sslCertificate: "SSL certificate",
whiteListIpText:
"Please white-list our IP address if the data source is not publicly accessible",
@ -74,6 +78,8 @@ export const postgreSqlText = {
guiOptionBulkUpdate: "Bulk update using primary key",
buttonTextTestConnection: "Test connection",
editButtonText: "Edit",
unableAcquireConnectionAlertText: "Unable to acquire a connection",
tabAdvanced: "Advanced",
labelNoEventhandler: "No event handlers",

View file

@ -3,24 +3,15 @@ import { postgreSqlSelector, airTableSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { airtableText } from "Texts/airTable";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import {
fillDataSourceTextField,
selectAndAddDataSource,
} from "Support/utils/postgreSql";
import {
deleteDatasource,
closeDSModal,
deleteAppandDatasourceAfterExecution,
} from "Support/utils/dataSource";
import { closeDSModal } from "Support/utils/dataSource";
import { dataSourceSelector } from "../../../../../constants/selectors/dataSource";
const data = {};
data.queryName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
const airTable_apiKey = Cypress.env("airTable_apikey");
const airTable_baseId = Cypress.env("airtabelbaseId");
const airTable_tableName = Cypress.env("airtable_tableName");
const airTable_recordID = Cypress.env("airtable_recordId");
describe("Data source Airtable", () => {
beforeEach(() => {
@ -54,72 +45,142 @@ describe("Data source Airtable", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
cy.get(postgreSqlSelector.buttonSave).verifyVisibleElement(
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dsName}-airtable`,
"airtable",
[
{
key: "personal_access_token",
value: `${Cypress.env("airTable_apikey")}`,
encrypted: true,
},
]
);
cy.reload();
cy.get(
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
)
.should("be.visible")
.click();
cy.get(
dataSourceSelector.labelFieldName(airtableText.ApiKey)
).verifyVisibleElement("have.text", `${airtableText.ApiKey}*`);
cy.get(postgreSqlSelector.labelEncryptedText).verifyVisibleElement(
"have.text",
postgreSqlText.buttonTextSave
postgreSqlText.labelEncrypted
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).should(
"be.visible"
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).click();
cy.verifyRequiredFieldValidation(airtableText.ApiKey, "rgb(226, 99, 103)");
cy.get(dataSourceSelector.textField(airtableText.ApiKey)).should(
"be.visible"
);
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",
postgreSqlText.whiteListIpText
);
cy.get(postgreSqlSelector.buttonCopyIp).verifyVisibleElement(
"have.text",
postgreSqlText.textCopy
);
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
cy.get(postgreSqlSelector.linkReadDocumentation).verifyVisibleElement(
"have.text",
postgreSqlText.readDocumentation
);
deleteDatasource(`cypress-${data.dsName}-airtable`);
cy.get(postgreSqlSelector.buttonTestConnection)
.verifyVisibleElement(
"have.text",
postgreSqlText.buttonTextTestConnection
)
.click();
cy.get(postgreSqlSelector.connectionFailedText).verifyVisibleElement(
"have.text",
postgreSqlText.couldNotConnect
);
cy.get(postgreSqlSelector.buttonSave)
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
airtableText.invalidAccessToken
);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
});
it("Should verify the functionality of AirTable connection form.", () => {
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
fillDataSourceTextField(
airtableText.ApiKey,
airtableText.apikeyPlaceholder,
Cypress.env("airTable_apikey")
);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dsName}-airtable`,
"airtable",
[
{
key: "personal_access_token",
value: "Invalid access token",
encrypted: true,
},
]
);
cy.get(
`[data-cy="cypress-${data.dsName}-airtable-button"]`
).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`);
deleteDatasource(`cypress-${data.dsName}-airtable`);
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(dataSourceSelector.connectionFailedText, {
timeout: 10000,
}).should("have.text", postgreSqlText.couldNotConnect);
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
airtableText.invalidAccessToken
);
cy.reload();
cy.apiUpdateGDS({
name: `cypress-${data.dsName}-airtable`,
options: [
{
key: "personal_access_token",
value: `${Cypress.env("airTable_apikey")}`,
encrypted: true,
},
],
});
cy.get(
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
});
it("Should able to run the query with valid conection", () => {
const airTable_apiKey = Cypress.env("airTable_apikey");
const airTable_baseId = Cypress.env("airtabelbaseId");
const airTable_tableName = Cypress.env("airtable_tableName");
const airTable_recordID = Cypress.env("airtable_recordId");
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
fillDataSourceTextField(
airtableText.ApiKey,
airtableText.apikeyPlaceholder,
airTable_apiKey
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dsName}-airtable`,
"airtable",
[
{
key: "personal_access_token",
value: `${airTable_apiKey}`,
encrypted: true,
},
]
);
cy.wait(1000);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.dsName}-airtable-button"]`
).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`);
cy.get(commonSelectors.dashboardIcon).click();
cy.get(commonSelectors.appCreateButton).click();
cy.get(commonSelectors.appNameInput).click().type(data.dsName);
cy.get(commonSelectors.createAppButton).click();
cy.skipWalkthrough();
cy.apiCreateApp(`${data.dsName}-airtable-app`);
cy.openApp();
cy.get('[data-cy="show-ds-popover-button"]').click();
cy.get(".css-4e90k9").type(`${data.dsName}`);
@ -280,10 +341,9 @@ describe("Data source Airtable", () => {
commonSelectors.toastMessage,
`Query (${data.queryName}) completed.`
);
deleteAppandDatasourceAfterExecution(
data.dsName,
`cypress-${data.dsName}-airtable`
);
cy.apiDeleteApp(`${data.dsName}-airtable-app`);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
});
});
});

View file

@ -66,10 +66,9 @@ describe("Data source BigQuery", () => {
`cypress-${data.dataSourceName}-bigquery`
);
cy.get('[data-cy="label-private-key"]').verifyVisibleElement(
"have.text",
firestoreText.labelPrivateKey
);
cy.get(
dataSourceSelector.labelFieldName(firestoreText.labelPrivateKey)
).verifyVisibleElement("have.text", "Private key*");
cy.get(".datasource-edit-btn").should("be.visible");
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",
@ -98,7 +97,7 @@ describe("Data source BigQuery", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
bigqueryText.errorInvalidEmailId
);
@ -110,38 +109,30 @@ describe("Data source BigQuery", () => {
});
it("Should verify the functionality of BigQuery connection form.", () => {
selectAndAddDataSource(
"databases",
bigqueryText.bigQuery,
data.dataSourceName
);
fillDataSourceTextField(
firestoreText.privateKey,
bigqueryText.placehlderPrivateKey,
`${JSON.stringify(Cypress.env("bigquery_pvt_key"))}`,
"contain",
{ parseSpecialCharSequences: false, delay: 0 }
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-bigquery`,
"bigquery",
[
{
key: "private_key",
value: `${JSON.stringify(Cypress.env("bigquery_pvt_key"))}`,
encrypted: true,
},
]
);
cy.get(
dataSourceSelector.dataSourceNameButton(
`cypress-${data.dataSourceName}-bigquery`
)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.dataSourceName}-bigquery-button"]`
).verifyVisibleElement(
"have.text",
`cypress-${data.dataSourceName}-bigquery`
);
deleteDatasource(`cypress-${data.dataSourceName}-bigquery`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-bigquery`);
});
});

View file

@ -101,7 +101,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Invalid URL"
);

View file

@ -127,7 +127,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Invalid URL"
);

View file

@ -115,7 +115,7 @@ describe("Data source DynamoDB", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
dynamoDbText.errorMissingRegion
);

View file

@ -122,7 +122,7 @@ describe("Data source Elasticsearch", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
elasticsearchText.errorConnectionRefused
);

View file

@ -94,7 +94,7 @@ describe("Data source Firestore", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
firestoreText.errorGcpKeyCouldNotBeParsed
);

View file

@ -118,7 +118,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Invalid URL"
);

View file

@ -125,7 +125,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
// cy.get('[data-cy="connection-alert-text"]').should("be.visible")
// cy.get(dataSourceSelector.connectionAlertText).should("be.visible")
deleteDatasource(`cypress-${data.dataSourceName}-mariadb`);
});

View file

@ -129,7 +129,7 @@ describe("Data source MongoDB", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
mongoDbText.errorConnectionRefused
);
@ -164,7 +164,7 @@ describe("Data source MongoDB", () => {
}).verifyVisibleElement("have.text", postgreSqlText.couldNotConnect, {
timeout: 95000,
});
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Cannot read properties of null (reading '2')"
);

View file

@ -27,7 +27,7 @@ describe("Data sources", () => {
.replaceAll("[^A-Za-z]", "");
});
it.skip("Should verify elements on connection form", () => {
it("Should verify elements on connection form with validation", () => {
cy.log(process.env.NODE_ENV);
cy.log(postgreSqlText.allDatabase());
cy.get(commonSelectors.globalDataSourceIcon).click();
@ -81,30 +81,147 @@ describe("Data sources", () => {
`cypress-${data.dataSourceName}-postgresql`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
cy.get(
dataSourceSelector.dropdownLabel(postgreSqlText.labelConnectionType)
).verifyVisibleElement("have.text", postgreSqlText.labelConnectionType);
cy.get(dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType))
.should("be.visible")
.click();
cy.contains(
`[id*="react-select-"]`,
postgreSqlText.connectionStringOption
).click();
cy.get(
dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType)
).should("be.visible");
cy.get(
dataSourceSelector.labelFieldName(postgreSqlText.connectionStringOption)
).verifyVisibleElement(
"have.text",
postgreSqlText.labelHost
`${postgreSqlText.connectionStringOption}*`
);
cy.get(postgreSqlSelector.labelPort).verifyVisibleElement(
cy.get(postgreSqlSelector.labelEncryptedText).verifyVisibleElement(
"have.text",
postgreSqlText.labelPort
postgreSqlText.labelEncrypted
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).should(
"be.visible"
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).click();
cy.verifyRequiredFieldValidation(
postgreSqlText.connectionStringOption,
"rgb(226, 99, 103)"
);
cy.get(
dataSourceSelector.textField(postgreSqlText.connectionStringOption)
).should("be.visible");
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",
postgreSqlText.whiteListIpText
);
cy.get(postgreSqlSelector.buttonCopyIp).verifyVisibleElement(
"have.text",
postgreSqlText.textCopy
);
cy.get(postgreSqlSelector.linkReadDocumentation).verifyVisibleElement(
"have.text",
postgreSqlText.readDocumentation
);
cy.get(postgreSqlSelector.buttonTestConnection)
.verifyVisibleElement(
"have.text",
postgreSqlText.buttonTextTestConnection
)
.click();
cy.get(postgreSqlSelector.connectionFailedText).verifyVisibleElement(
"have.text",
postgreSqlText.couldNotConnect
);
cy.get(postgreSqlSelector.buttonSave)
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
postgreSqlText.unableAcquireConnectionAlertText
);
cy.get(dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType))
.should("be.visible")
.click();
cy.contains(
`[id*="react-select-"]`,
postgreSqlText.manualConnectionOption
).click();
cy.get(
dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType)
).should("be.visible");
const requiredFields = [
postgreSqlText.labelHost,
postgreSqlText.labelPort,
postgreSqlText.labelUserName,
postgreSqlText.labelPassword,
];
const sections = [
postgreSqlText.labelHost,
postgreSqlText.labelPort,
postgreSqlText.labelDbName,
postgreSqlText.labelUserName,
postgreSqlText.labelPassword,
postgreSqlText.labelConnectionOptions,
];
sections.forEach((section) => {
if (section === postgreSqlText.labelConnectionOptions) {
cy.get(dataSourceSelector.keyInputField(section, 0)).should(
"be.visible"
);
cy.get(dataSourceSelector.valueInputField(section, 0)).should(
"be.visible"
);
cy.get(dataSourceSelector.deleteButton(section, 0)).should(
"be.visible"
);
cy.get(dataSourceSelector.addMoreButton(section)).should("be.visible");
} else if (requiredFields.includes(section)) {
cy.get(dataSourceSelector.labelFieldName(section)).verifyVisibleElement(
"have.text",
`${section}*`
);
cy.get(dataSourceSelector.textField(section)).should("be.visible");
if (section === postgreSqlText.labelPassword) {
cy.get(
dataSourceSelector.button(postgreSqlText.editButtonText)
).click();
cy.verifyRequiredFieldValidation(section, "rgb(215, 45, 57)");
} else {
cy.get(dataSourceSelector.textField(section)).click();
cy.get(commonSelectors.textField(section)).should(
"have.css",
"border-color",
"rgba(0, 0, 0, 0)"
);
cy.get(dataSourceSelector.textField(section))
.type("123")
.clear()
.blur();
cy.verifyRequiredFieldValidation(section, "rgb(215, 45, 57)");
}
} else {
cy.get(dataSourceSelector.labelFieldName(section)).verifyVisibleElement(
"have.text",
section
);
cy.get(dataSourceSelector.textField(section)).should("be.visible");
}
});
cy.get(postgreSqlSelector.labelSsl).verifyVisibleElement(
"have.text",
postgreSqlText.labelSSL
);
cy.get(postgreSqlSelector.labelDbName).verifyVisibleElement(
"have.text",
postgreSqlText.labelDbName
);
cy.get(postgreSqlSelector.labelUserName).verifyVisibleElement(
"have.text",
postgreSqlText.labelUserName
);
cy.get(postgreSqlSelector.labelPassword).verifyVisibleElement(
"have.text",
postgreSqlText.labelPassword
);
cy.get(postgreSqlSelector.sslToggleInput).should("be.visible");
cy.get(postgreSqlSelector.labelSSLCertificate).verifyVisibleElement(
"have.text",
postgreSqlText.sslCertificate
@ -132,72 +249,85 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.couldNotConnect
);
cy.get(postgreSqlSelector.buttonSave).verifyVisibleElement(
cy.get(postgreSqlSelector.buttonSave)
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
postgreSqlText.buttonTextSave
"connect ECONNREFUSED 127.0.0.1:5432"
);
cy.get('[data-cy="connection-alert-text"]').should("be.visible");
deleteDatasource(`cypress-${data.dataSourceName}-postgresql`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-postgresql`);
});
it.skip("Should verify the functionality of PostgreSQL connection form.", () => {
selectAndAddDataSource(
"databases",
postgreSqlText.postgreSQL,
data.dataSourceName
it("Should verify the functionality of PostgreSQL connection form.", () => {
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-manual-pgsql`,
"postgresql",
[
{ key: "connection_type", value: "manual", encrypted: false },
{ key: "host", value: `${Cypress.env("pg_host")}`, encrypted: false },
{ key: "port", value: 5432, encrypted: false },
{ key: "ssl_enabled", value: false, encrypted: false },
{ key: "database", value: "postgres", encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{
key: "username",
value: `${Cypress.env("pg_user")}`,
encrypted: false,
},
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
{ key: "ca_cert", value: null, encrypted: true },
{ key: "client_key", value: null, encrypted: true },
{ key: "client_cert", value: null, encrypted: true },
{ key: "root_cert", value: null, encrypted: true },
{ key: "connection_string", value: null, encrypted: true },
]
);
fillDataSourceTextField(
postgreSqlText.labelHost,
postgreSqlText.placeholderEnterHost,
Cypress.env("pg_host")
);
fillDataSourceTextField(
postgreSqlText.labelPort,
postgreSqlText.placeholderEnterPort,
"5432"
);
cy.get('[data-cy="-toggle-input"]').then(($el) => {
if ($el.is(":checked")) {
cy.get('[data-cy="-toggle-input"]').uncheck();
}
});
fillDataSourceTextField(
postgreSqlText.labelDbName,
postgreSqlText.placeholderNameOfDB,
"postgres"
);
fillDataSourceTextField(
postgreSqlText.labelUserName,
postgreSqlText.placeholderEnterUserName,
"postgres"
);
fillDataSourceTextField(
postgreSqlText.labelPassword,
"**************",
Cypress.env("pg_password")
);
cy.get(
dataSourceSelector.dataSourceNameButton(
`cypress-${data.dataSourceName}-manual-pgsql`
)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-manual-pgsql`);
cy.reload();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-string-pgsql`,
"postgresql",
[
{ key: "connection_type", value: "string", encrypted: false },
{
key: "connection_string",
value: `${Cypress.env("pg_string")}`,
encrypted: true,
},
]
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.dataSourceName}-postgresql-button"]`
).verifyVisibleElement(
"have.text",
`cypress-${data.dataSourceName}-postgresql`
);
deleteDatasource(`cypress-${data.dataSourceName}-postgresql`);
dataSourceSelector.dataSourceNameButton(
`cypress-${data.dataSourceName}-string-pgsql`
)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-string-pgsql`);
});
it.skip("Should verify elements of the Query section.", () => {

View file

@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { redisText } from "Texts/redis";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
fillDataSourceTextField,
@ -96,7 +96,7 @@ describe("Data source Redis", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorMaxRetries
);
@ -137,7 +137,7 @@ describe("Data source Redis", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorInvalidUserOrPassword
);
@ -152,7 +152,7 @@ describe("Data source Redis", () => {
"108299"
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorPort
);
@ -170,7 +170,7 @@ describe("Data source Redis", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorInvalidUserOrPassword
);
@ -187,7 +187,7 @@ describe("Data source Redis", () => {
"redis"
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorInvalidUserOrPassword
);

View file

@ -329,9 +329,9 @@ describe("Data source Rest API", () => {
);
cy.contains("Save").click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Data Source Saved");
deleteDatasource(`cypress-${data.dataSourceName}-restapi`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it("Should verify basic connection for Rest API", () => {
it("Should verify connection response for all methods", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
@ -367,88 +367,388 @@ describe("Data source Rest API", () => {
);
cy.reload();
cy.apiCreateApp(`${fake.companyName}-restAPI-App`);
cy.apiCreateApp(`${fake.companyName}-restAPI-CURD-App`);
cy.openApp();
createAndRunRestAPIQuery(
"get_restapi",
`cypress-${data.dataSourceName}-restapi`,
"GET",
"/api/users"
);
createAndRunRestAPIQuery(
"post_restapi",
`cypress-${data.dataSourceName}-restapi`,
"POST",
"",
[["Content-Type", "application/json"]],
[],
{
price: 200,
name: "Violin",
},
true,
"/api/users"
);
createAndRunRestAPIQuery({
queryName: "get_beeceptor_data",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/api/users",
run: true,
});
createAndRunRestAPIQuery({
queryName: "post_restapi",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "POST",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"price": 200,"name": "Violin"}',
urlSuffix: "/api/users",
expectedResponseShape: { price: 200, name: "Violin", id: true },
});
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
(postResponseID) => {
const id1 = postResponseID.id;
createAndRunRestAPIQuery(
"put_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"PUT",
"",
[["Content-Type", "application/json"]],
[],
{
price: 500,
name: "Guitar",
},
true,
`/api/users/${id1}`
);
}
);
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
(putResponseID) => {
const id2 = putResponseID.id;
createAndRunRestAPIQuery(
"patch_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"PATCH",
"",
[["Content-Type", "application/json"]],
[],
{ price: 999 },
true,
`/api/users/${id2}`
);
}
);
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
(patchResponseID) => {
const id3 = patchResponseID.id;
createAndRunRestAPIQuery(
"get_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"GET",
"",
[],
[],
true,
`/api/users/${id3}`
);
createAndRunRestAPIQuery(
"delete_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"DELETE",
"",
[],
[],
true,
`/api/users/${id3}`
);
createAndRunRestAPIQuery({
queryName: "put_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "PUT",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"price": 500,"name": "Guitar"}',
urlSuffix: `/api/users/${id1}`,
expectedResponseShape: { price: 500, name: "Guitar", id: id1 },
});
createAndRunRestAPIQuery({
queryName: "patch_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "PATCH",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"price": 999 }',
urlSuffix: `/api/users/${id1}`,
run: true,
expectedResponseShape: { price: 999, id: id1 },
});
createAndRunRestAPIQuery({
queryName: "get_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: `/api/users/${id1}`,
run: true,
expectedResponseShape: { price: 999, name: "Guitar", id: id1 },
});
createAndRunRestAPIQuery({
queryName: "delete_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "DELETE",
urlSuffix: `/api/users/${id1}`,
run: true,
expectedResponseShape: { success: true },
});
}
);
cy.apiDeleteApp(`${fake.companyName}-restAPI-CURD-App`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it("Should verify response for basic authentication type connection", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
"restapi",
[
{ key: "url", value: "https://httpbin.org" },
{ key: "auth_type", value: "basic" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_id", value: "" },
{
key: "client_secret",
encrypted: true,
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
},
{ key: "audience", value: "" },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "user", encrypted: false },
{ key: "password", value: "pass", encrypted: true },
{
key: "bearer_token",
encrypted: true,
credential_id: "21caf3cb-dbde-43c7-9f42-77feffb63062",
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
{ key: "url_parameters", value: [["", ""]], encrypted: false },
{ key: "tokenData", encrypted: false },
]
);
cy.reload();
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.apiCreateApp(`${fake.companyName}-restAPI-Basic-App`);
createAndRunRestAPIQuery({
queryName: "get_basic_auth_valid",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/basic-auth/user/pass",
expectedResponseShape: { authenticated: true, user: "user" },
});
createAndRunRestAPIQuery({
queryName: "get_basic_auth_invalid",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/basic-auth/invaliduser/invalidpass",
});
cy.apiDeleteApp(`${fake.companyName}-restAPI-Basic-App`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it("Should verify response for bearer authentication type connection", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
"restapi",
[
{ key: "url", value: "https://httpbin.org" },
{ key: "auth_type", value: "bearer" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_id", value: "" },
{
key: "client_secret",
encrypted: true,
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
},
{ key: "audience", value: "" },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
{
key: "bearer_token",
value: "my-token-123",
encrypted: true,
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
{ key: "url_parameters", value: [["", ""]], encrypted: false },
{ key: "tokenData", encrypted: false },
]
);
cy.reload();
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.apiCreateApp(`${fake.companyName}-restAPI-Bearer-App`);
cy.openApp();
createAndRunRestAPIQuery({
queryName: "get_bearer_auth_valid",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/bearer",
expectedResponseShape: { authenticated: true, token: "my-token-123" },
});
cy.apiDeleteApp(`${fake.companyName}-restAPI-Bearer-App`);
cy.intercept("GET", "api/data_sources?**").as("datasource");
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi-invalid`,
"restapi",
[
{ key: "url", value: "https://httpbin.org" },
{ key: "auth_type", value: "bearer" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_id", value: "" },
{
key: "client_secret",
encrypted: true,
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
},
{ key: "audience", value: "" },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
{
key: "bearer_token",
value: "",
encrypted: true,
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
{ key: "url_parameters", value: [["", ""]], encrypted: false },
{ key: "tokenData", encrypted: false },
]
);
cy.apiCreateApp(`${fake.companyName}-restAPI-Bearer-invalid`);
cy.openApp();
createAndRunRestAPIQuery({
queryName: "get_bearer_auth_invalid",
dsName: `cypress-${data.dataSourceName}-restapi-invalid`,
method: "GET",
urlSuffix: "/bearer",
});
cy.apiDeleteApp(`${fake.companyName}-restAPI-Bearer-invalid`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it.skip("Should verify response for authentication code grant type connection", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
"restapi",
[
{
key: "url",
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/api/v2/users",
},
{ key: "auth_type", value: "oauth2" },
{ key: "grant_type", value: "client_credentials" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{
key: "access_token_url",
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/oauth/token",
},
{ key: "client_id", value: "JBDuuLU9vaSTP6Do7zYSkw0GvVgWhfyZ" },
{
key: "client_secret",
encrypted: true,
credential_id: "a6d26607-4d09-42a2-8bc0-e5c185c7c2f7",
},
{
key: "audience",
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/api/v2/",
},
{ key: "scopes", value: "" },
{ key: "username", value: "", encrypted: false },
{
key: "password",
encrypted: true,
credential_id: "4502a906-b512-447a-a128-39f67e9778d2",
},
{
key: "bearer_token",
encrypted: true,
credential_id: "c94262c7-d2c5-4d7f-96f8-657689f2b1f0",
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
]
);
cy.reload();
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.apiCreateApp(`${fake.companyName}-client-Grant-RestAPI`);
});
it("Should verify response for content-type", () => {
cy.apiCreateApp(`${fake.companyName}-restAPI-Content-App`);
createAndRunRestAPIQuery({
queryName: "post_json",
dsName: "restapidefault",
method: "POST",
url: "https://jsonplaceholder.typicode.com/posts",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"title": "foo","body": "bar","userId": 1}',
run: true,
urlSuffix: "",
expectedResponseShape: { id: true, title: "foo", body: "bar", userId: 1 },
});
createAndRunRestAPIQuery({
queryName: "post_raw_text",
dsName: "restapidefault",
method: "POST",
url: "https://httpbin.org/post",
headersList: [["Content-Type", "text/plain"]],
rawBody: "This is plain text content",
jsonBody: null,
run: true,
expectedResponseShape: { data: "This is plain text content" },
});
createAndRunRestAPIQuery({
queryName: "post_form_urlencoded",
dsName: "restapidefault",
method: "POST",
url: "https://httpbin.org/post",
headersList: [["Content-Type", "application/x-www-form-urlencoded"]],
bodyList: [
["name", "Jane"],
["age", "30"],
],
expectedResponseShape: {
"form.name": "Jane",
"form.age": "30",
},
});
createAndRunRestAPIQuery({
queryName: "post_xml_soap",
dsName: "restapidefault",
method: "POST",
url: "http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL",
headersList: [["Content-Type", "text/xml; charset=utf-8"]],
rawBody: `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ListOfContinentsByName xmlns="http://www.oorsprong.org/websamples.countryinfo">
</ListOfContinentsByName>
</soap:Body>
</soap:Envelope>`,
jsonBody: null,
bodyList: [],
cookiesList: [["session", "abc123"]],
paramsList: [["lang", "en"]],
run: true,
shouldSucceed: true,
expectedResponseShape: {},
});
// createAndRunRestAPIQuery({
// queryName: "post_text_csv",
// dsName: "restapidefault",
// method: "POST",
// url: `https://tejasvi.free.beeceptor.com/csv-upload`,
// headersList: [["Content-Type", "text/csv"]],
// rawBody:
// "id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com",
// expectedResponseShape: {
// data: '{\n "status": "ok",\n "message": "File uploaded successfully",\n "body": id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com\n}',
// },
// });
// const filename = "tooljet.png";
// createAndRunRestAPIQuery({
// queryName: "upload_image",
// dsName: "restapidefault",
// method: "POST",
// url: `https://tejasvi.free.beeceptor.commultipart-upload`,
// headersList: [["Content-Type", "multipart/form-data"]],
// bodyList: [
// ["Image_File", "fixture:Image/tooljet.png"],
// ["filename", filename],
// ],
// expectedResponseShape: {
// filename: filename,
// },
// });
});
});

View file

@ -120,7 +120,7 @@ describe("Data sources AWS S3", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.alertRegionIsMissing
);
@ -144,7 +144,7 @@ describe("Data sources AWS S3", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.alertRegionIsMissing
);
@ -170,7 +170,7 @@ describe("Data sources AWS S3", () => {
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.alertInvalidUrl
);
@ -188,7 +188,7 @@ describe("Data sources AWS S3", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.accessKeyError
);
@ -207,7 +207,7 @@ describe("Data sources AWS S3", () => {
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.sinatureError
);

View file

@ -105,7 +105,7 @@ describe("Data source SMTP", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
"Invalid credentials"
);

View file

@ -128,7 +128,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
"Invalid account. The specified value must be a valid subdomain string."
);

View file

@ -132,7 +132,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Failed to connect to localhost:1433 - Could not connect (sequence)"
);

View file

@ -96,7 +96,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
"Ensure that apiKey is set"
);

View file

@ -114,17 +114,30 @@ describe("App Version", () => {
cy.wait(3000);
// cy.reload();
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", { timeout: 10000 });
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", {
timeout: 10000,
});
// Preview and release verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v2");
cy.openApp("", Cypress.env("workspaceId"), Cypress.env("appId"), commonWidgetSelector.draggableWidget("text1"));
cy.openApp(
"",
Cypress.env("workspaceId"),
Cypress.env("appId"),
commonWidgetSelector.draggableWidget("text1")
);
releasedVersionAndVerify("v2");
});
it("should verify version management with components and queries", () => {
// Initial setup with component and datasource
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
data.datasourceName,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
cy.apiAddComponentToApp(
data.appName,
"text1",
@ -134,19 +147,15 @@ describe("App Version", () => {
);
cy.waitForAutoSave();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
data.datasourceName,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
createRestAPIQuery(data.query1, data.datasourceName, "", "", "/1", true);
// Version v2 creation and verification
navigateToCreateNewVersionModal("v1");
createNewVersion(["v2"], "v1");
cy.get(commonWidgetSelector.draggableWidget("text1"))
.verifyVisibleElement("have.text", "Leanne Graham");
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
// Modify v2 with new components and queries
@ -170,67 +179,79 @@ describe("App Version", () => {
create: { version: "v3", from: "v2" },
verify: {
component: { selector: "textInput", value: "Ervin Howell" },
query: data.query2
}
query: data.query2,
},
},
{
create: { version: "v4", from: "v1" },
verify: {
component: { selector: "text1", text: "Leanne Graham" },
query: data.query1
}
query: data.query1,
},
},
{
create: { version: "v5", from: "v3" },
verify: {
component: { selector: "textInput", value: "Ervin Howell" },
query: data.query2
}
}
query: data.query2,
},
},
];
versionChecks.forEach(check => {
versionChecks.forEach((check) => {
navigateToCreateNewVersionModal(check.create.from);
createNewVersion([check.create.version], check.create.from);
if (check.verify.component.value) {
cy.get(commonWidgetSelector.draggableWidget(check.verify.component.selector))
.verifyVisibleElement("have.value", check.verify.component.value);
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
).verifyVisibleElement("have.value", check.verify.component.value);
} else {
cy.get(commonWidgetSelector.draggableWidget(check.verify.component.selector))
.verifyVisibleElement("have.text", check.verify.component.text);
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
).verifyVisibleElement("have.text", check.verify.component.text);
}
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should("be.visible");
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should(
"be.visible"
);
});
// Release and version state verification
releasedVersionAndVerify("v5");
cy.get(appVersionSelectors.currentVersionField("v5"))
.should("have.class", "color-light-green");
cy.get(appVersionSelectors.currentVersionField("v5")).should(
"have.class",
"color-light-green"
);
// Version switching and component verification
cy.get(appVersionSelectors.currentVersionField("v5")).click();
cy.contains(`[id*="react-select-"]`, "v4").click();
cy.get(appVersionSelectors.currentVersionField("v4"))
.should("not.have.class", "color-light-green");
cy.get(commonWidgetSelector.draggableWidget("text1"))
.verifyVisibleElement("have.text", "Leanne Graham");
cy.get(appVersionSelectors.currentVersionField("v4")).should(
"not.have.class",
"color-light-green"
);
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
// Preview and version switching verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v4");
cy.get(commonWidgetSelector.draggableWidget("text1"))
.verifyVisibleElement("have.text", "Leanne Graham");
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v4", "v5");
cy.get(commonWidgetSelector.draggableWidget("textInput"))
.verifyVisibleElement("have.value", "Ervin Howell");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
//url validation should be added after bug fix
// cy.url().should("include", "/home?version=v5");
});
});

View file

@ -135,7 +135,7 @@ export const resolveHost = () => {
const baseUrl = Cypress.config("baseUrl");
const urlMapping = {
"http://localhost:8082": "http://localhost:8082",
"http://localhost:3000": "http://localhost:3000",
"http://localhost:3000/apps": "http://localhost:3000/apps",
"http://localhost:4001": "http://localhost:3000",
"http://localhost:4001/apps": "http://localhost:3000/apps",

View file

@ -255,7 +255,7 @@ export const createRestAPIQuery = (
}).then((response) => {
const editingVersionId = response.body.editing_version.id;
const data_source_id = Cypress.env(kind);
const data_source_id = Cypress.env(`${dsName}`);
const requestBody = {
app_id: Cypress.env("appId"),

View file

@ -1,19 +1,31 @@
export const createAndRunRestAPIQuery = (
export const createAndRunRestAPIQuery = ({
queryName,
dsName,
method = "GET",
url = "",
urlSuffix = "",
headersList = [],
bodyList = [],
jsonBody = null,
rawBody = null,
cookiesList = [],
paramsList = [],
run = true,
urlSuffix = ""
) => {
expectedResponseShape = {},
authType = "",
authToken = "",
}) => {
cy.getCookie("tj_auth_token").then((cookie) => {
const headers = {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
};
// if (authType === "bearer" || authType === "oauth2") {
// headers["Authorization"] = `Bearer ${authToken}`;
// } else if (authType === "basic") {
// headers["Authorization"] = `Basic ${btoa(authToken)}`;
// }
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/apps/${Cypress.env("appId")}`,
@ -27,84 +39,108 @@ export const createAndRunRestAPIQuery = (
url: `${Cypress.env("server_host")}/api/data-sources/${Cypress.env("workspaceId")}/environments/${currentEnvironmentId}/versions/${editingVersionId}`,
headers,
}).then((dsResponse) => {
expect(dsResponse.status).to.eq(200);
const dataSource = dsResponse.body.data_sources.find(
(ds) => ds.name === dsName
);
if (!dataSource) {
throw new Error(`Data source '${dsName}' not found.`);
}
const data_source_id = dataSource.id;
const useJsonBody =
["POST", "PATCH", "PUT"].includes(method.toUpperCase()) &&
jsonBody !== null;
const useJson = jsonBody !== null;
const useRaw = rawBody !== null;
const useForm = bodyList?.length && !useJson && !useRaw;
const queryOptions = {
method: method.toLowerCase(),
url: url + urlSuffix,
url_params: [["", ""]],
url_params: paramsList.length ? paramsList : [["", ""]],
headers: headersList.length ? headersList : [["", ""]],
body: !useJsonBody && bodyList.length ? bodyList : [["", ""]],
json_body: useJsonBody ? jsonBody : null,
body_toggle: useJsonBody,
cookies: cookiesList.length ? cookiesList : [["", ""]],
body: useForm ? bodyList : [["", ""]],
json_body: useJson ? jsonBody : null,
raw_body: useRaw ? rawBody : "",
body_toggle: useJson || useRaw,
runOnPageLoad: run,
transformationLanguage: "javascript",
enableTransformation: false,
};
const requestBody = {
app_id: Cypress.env("appId"),
app_version_id: editingVersionId,
name: queryName,
kind: "restapi",
options: queryOptions,
data_source_id,
data_source_id: dataSource.id,
plugin_id: null,
};
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/data-queries/data-sources/${data_source_id}/versions/${editingVersionId}`,
url: `${Cypress.env("server_host")}/api/data-queries/data-sources/${dataSource.id}/versions/${editingVersionId}`,
headers,
body: requestBody,
}).then((createResponse) => {
expect(createResponse.status).to.equal(201);
expect(createResponse.status).to.eq(201);
const queryId = createResponse.body.id;
cy.log("Query created successfully:", queryId);
const createdOptions = createResponse.body.options;
expect(createdOptions.method).to.equal(queryOptions.method);
expect(createdOptions.url).to.equal(queryOptions.url);
expect(createdOptions.headers).to.deep.equal(queryOptions.headers);
if (useJsonBody) {
expect(createdOptions.json_body).to.deep.equal(
queryOptions.json_body
);
expect(createdOptions.body_toggle).to.equal(true);
} else {
expect(createdOptions.body).to.deep.equal(queryOptions.body);
expect(createdOptions.body_toggle).to.equal(false);
}
expect(createdOptions.runOnPageLoad).to.equal(run);
cy.log("Metadata verified successfully");
if (run) {
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/data-queries/${queryId}/run`,
headers,
failOnStatusCode: false,
}).then((runResponse) => {
expect([200, 201]).to.include(runResponse.status);
cy.log("Query executed successfully:", runResponse.body);
if (runResponse.body?.data.id) {
cy.writeFile("cypress/fixtures/restAPI/storedId.json", {
id: runResponse.body.data.id,
});
cy.log("Stored ID:", runResponse.body.data.id);
const responseData = runResponse.body?.data;
const requestHeaders =
runResponse.body?.metadata?.request?.headers || {};
if (runResponse.body.status === "ok") {
expect([200, 201]).to.include(runResponse.status);
cy.log("Response:", responseData);
if (
expectedResponseShape &&
typeof expectedResponseShape === "object"
) {
Object.entries(expectedResponseShape).forEach(
([path, expected]) => {
const value = path
.split(".")
.reduce((obj, key) => obj?.[key], responseData);
if (expected === true) {
expect(value).to.not.be.undefined;
} else {
expect(value).to.eq(expected);
}
}
);
}
const expectedContentType = headersList.find(
([key]) => key.toLowerCase() === "content-type"
)?.[1];
if (expectedContentType && requestHeaders["content-type"]) {
expect(requestHeaders["content-type"]).to.include(
expectedContentType
);
}
if (Array.isArray(responseData)) {
responseData.forEach((item) => {
expect(item).to.have.any.keys("id", "name", "price");
});
}
if (responseData?.id) {
cy.writeFile("cypress/fixtures/restAPI/storedId.json", {
id: responseData.id,
});
}
} else if (runResponse.body.status === "failed") {
expect(runResponse.body.message).to.eq(
"Query could not be completed"
);
const statusCode =
runResponse.body?.metadata?.response?.statusCode;
expect([400, 401, 403, 404, 500]).to.include(statusCode);
cy.log(
"Failure validated as expected with status:",
statusCode
);
}
});
}

View file

@ -1 +1 @@
3.13.0
3.14.0

@ -1 +1 @@
Subproject commit 777446d71e78e5941d34353606a12d982820438f
Subproject commit aa3c4f603f549337fc88a772a6a31e18eaf38701

View file

@ -1,12 +1,13 @@
import React from 'react';
import cx from 'classnames';
import { pluginsService, marketplaceService } from '@/_services';
import { pluginsService, marketplaceService, globalDatasourceService } from '@/_services';
import { toast } from 'react-hot-toast';
import Spinner from '@/_ui/Spinner';
import { capitalizeFirstLetter, useTagsByPluginId } from './utils';
import { ConfirmDialog } from '@/_components';
import Icon from '@/_ui/Icon/SolidIcons';
import config from 'config';
import Modal from '@/HomePage/Modal';
export const InstalledPlugins = () => {
const [allPlugins, setAllPlugins] = React.useState([]);
@ -81,6 +82,7 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
const [updating, setUpdating] = React.useState(false);
const [isDeleteModalVisible, setDeleteModalVisibility] = React.useState(false);
const [isDeletingPlugin, setDeletingPlugin] = React.useState(false);
const [showDependentQueriesInfo, setShowDependentQueriesInfo] = React.useState(false);
const darkMode = localStorage.getItem('darkMode') === 'true';
const { id, name, pluginId } = plugin;
@ -140,6 +142,21 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
toast.success(`${capitalizeFirstLetter(name)} reloaded`);
};
const getQueriesLinkedToMarketplacePlugin = (plugin) => {
globalDatasourceService
.getQueriesLinkedToMarketplacePlugin(plugin.id)
.then((data) => {
if (data?.dependent_queries) {
setShowDependentQueriesInfo(true);
} else {
setDeleteModalVisibility(true);
}
})
.catch(({ error }) => {
toast.error(error);
});
};
const pluginDeleteMessage = (
<>
Deleting <strong>{capitalizeFirstLetter(name)}</strong> plugin will result in the permanent removal of all
@ -150,6 +167,15 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
return (
<>
<Modal
title="Dependent queries found!"
show={showDependentQueriesInfo}
closeModal={() => setShowDependentQueriesInfo(false)}
>
<div className="mt-3 mb-3">
Cannot delete the <b>{plugin?.name}</b> plugin as it is used in the apps
</div>
</Modal>
<ConfirmDialog
title={'Delete plugin'}
show={isDeleteModalVisible}
@ -238,7 +264,7 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
<div className="col-auto">
<div
className={cx('cursor-pointer link-primary', { disabled: updating })}
onClick={() => setDeleteModalVisibility(true)}
onClick={() => getQueriesLinkedToMarketplacePlugin(plugin)}
>
Remove
</div>

View file

@ -17,6 +17,11 @@ export const MarketplaceCard = ({ id, name, repo, description, version, isInstal
}, [isInstalled]);
const installPlugin = async () => {
if (installed) {
toast.error(`${capitalizeFirstLetter(name)} is already installed.`);
return;
}
const body = {
id,
name,

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useLayoutEffect, useRef } from 'react';
import { openapiService } from '@/_services';
import Select from '@/_ui/Select';
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
@ -110,7 +110,7 @@ const ApiEndpointInput = (props) => {
if (isEmpty(paths)) return [];
const pathGroups = Object.keys(paths).reduce((acc, path) => {
const operations = Object.keys(paths[path]);
const operations = Object.keys(paths[path]).filter((op) => Object.keys(operationColorMapping).includes(op));
const category = path.split('/')[2];
operations.forEach((operation) => categorizeOperations(operation, path, acc, category));
return acc;
@ -135,7 +135,7 @@ const ApiEndpointInput = (props) => {
{loadingSpec && (
<div className="p-3">
<div className="spinner-border spinner-border-sm text-azure mx-2" role="status"></div>
{props.t('stripe', 'Please wait while we load the OpenAPI specification.')}
<span>Please wait while we load the OpenAPI specification.</span>
</div>
)}
{options && !loadingSpec && (
@ -227,57 +227,64 @@ const RenderParameterFields = ({ parameters, type, label, options, changeParam,
}
const paramLabelWithDescription = (param) => {
const label = type === 'request' ? param : param.name;
const description = type === 'request' ? parameters[param]?.description : param.description;
return (
<ToolTip message={type === 'request' ? DOMPurify.sanitize(parameters[param].description) : param.description}>
<div className="cursor-help">
<input
type="text"
value={type === 'request' ? param : param.name}
className="form-control form-control-underline"
placeholder="key"
disabled
/>
<ToolTip message={DOMPurify.sanitize(description)}>
<div className="cursor-help d-flex align-items-center">
<AutoWidthText value={label} className="form-control form-control-underline" />
</div>
</ToolTip>
);
};
const paramLabelWithoutDescription = (param) => {
return (
<input
type="text"
value={type === 'request' ? param : param.name}
className="form-control"
placeholder="key"
disabled
/>
);
};
const label = type === 'request' ? param : param.name;
const paramType = (param) => {
return (
<div className="p-2 text-muted">
{type === 'query' &&
param?.schema?.anyOf &&
param?.schema?.anyOf.map((type, i) =>
i < param.schema?.anyOf.length - 1
? type.type.substring(0, 3).toUpperCase() + '|'
: type.type.substring(0, 3).toUpperCase()
)}
{(type === 'path' || (type === 'query' && !param?.schema?.anyOf)) &&
param?.schema?.type?.substring(0, 3).toUpperCase()}
{type === 'request' && parameters[param].type?.substring(0, 3).toUpperCase()}
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
<AutoWidthText value={label} className="form-control" />
</div>
);
};
const paramType = (param) => {
let paramTypeValue;
if (type === 'query') {
if (param?.schema?.anyOf) {
return (
<div className="p-2 text-muted">
{param.schema.anyOf.map((typeObj, i) =>
i < param.schema.anyOf.length - 1
? (typeObj.type || '').toString().substring(0, 3).toUpperCase() + '|'
: (typeObj.type || '').toString().substring(0, 3).toUpperCase()
)}
</div>
);
}
paramTypeValue = param?.schema?.type;
} else if (type === 'path') {
paramTypeValue = param?.schema?.type;
} else if (type === 'request') {
paramTypeValue = parameters[param]?.type;
}
const displayType = Array.isArray(paramTypeValue) ? paramTypeValue[0] : paramTypeValue;
return <div className="p-2 text-muted">{displayType?.toString().substring(0, 3).toUpperCase() || ''}</div>;
};
const paramDetails = (param) => {
return (
<div className="col-auto d-flex field field-width-179 align-items-center">
{(type === 'request' && parameters[param].description) || param?.description
? paramLabelWithDescription(param)
: paramLabelWithoutDescription(param)}
{param.required && <span className="text-danger fw-bold">*</span>}
<div className="col-auto d-flex field field-width-179 align-items-center justify-content-between">
<div className="d-inline-flex align-items-center gap-3">
{(type === 'request' && parameters[param].description) || param?.description
? paramLabelWithDescription(param)
: paramLabelWithoutDescription(param)}
{param.required && <span className="text-danger fw-bold">*</span>}
</div>
{paramType(param)}
</div>
);
@ -359,3 +366,34 @@ RenderParameterFields.propTypes = {
removeParam: PropTypes.func,
darkMode: PropTypes.bool,
};
const AutoWidthText = ({ value, className }) => {
const spanRef = useRef(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
if (spanRef.current) {
setWidth(spanRef.current.offsetWidth);
}
}, [value]);
return (
<div className={className} style={{ display: 'inline-block', width: width ? `${width}px` : 'auto' }}>
<span
ref={spanRef}
style={{
position: 'absolute',
visibility: 'hidden',
whiteSpace: 'pre',
fontSize: '12px',
fontFamily: 'inherit',
fontWeight: 400,
lineHeight: '20px',
}}
>
{value}
</span>
{value}
</div>
);
};

View file

@ -245,7 +245,7 @@ const DynamicForm = ({
encrypted,
placeholders = {},
editorType = 'basic',
specUrl = '',
spec_url = '',
disabled = false,
buttonText,
text,
@ -486,7 +486,7 @@ const DynamicForm = ({
};
case 'react-component-api-endpoint':
return {
specUrl: specUrl,
specUrl: spec_url,
optionsChanged,
options,
darkMode,

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { datasourceService } from '@/_services';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-hot-toast';
@ -15,8 +15,16 @@ const Slack = ({
isDisabled,
}) => {
const [authStatus, setAuthStatus] = useState(null);
const whiteLabelText = retrieveWhiteLabelText();
const [whiteLabelText, setWhiteLabelText] = useState('');
const plugin_id = selectedDataSource?.plugin?.id;
const { t } = useTranslation();
useEffect(() => {
async function fetchLabel() {
const text = await retrieveWhiteLabelText();
setWhiteLabelText(text);
}
fetchLabel();
}, []);
function authGoogle() {
const provider = 'slack';
@ -29,7 +37,7 @@ const Slack = ({
}
datasourceService
.fetchOauth2BaseUrl(provider)
.fetchOauth2BaseUrl(provider, plugin_id, {})
.then((data) => {
const authUrl = `${data.url}&scope=${scope}&access_type=offline&prompt=select_account`;

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Input from '@/_ui/Input';
@ -18,17 +18,26 @@ const Zendesk = ({
isDisabled,
optionsChanged,
}) => {
const [whiteLabelText, setWhiteLabelText] = useState('');
const [authStatus, setAuthStatus] = useState(null);
const whiteLabelText = retrieveWhiteLabelText();
useEffect(() => {
async function fetchLabel() {
const text = await retrieveWhiteLabelText();
setWhiteLabelText(text);
}
fetchLabel();
}, []);
function authZendesk() {
const provider = 'zendesk';
setAuthStatus('waiting_for_url');
const scope = options?.access_type?.value === 'read' ? 'read' : 'read%20write';
const subDomain = options?.subdomain?.value;
const client_id = options?.client_id?.value;
try {
const authUrl = `https://${options?.subdomain?.value}.zendesk.com/oauth/authorizations/new?response_type=code&client_id=${options?.client_id?.value}&redirect_uri=${window.location.origin}/oauth2/authorize&scope=${scope}`;
const authUrl = `https://${subDomain}.zendesk.com/oauth/authorizations/new?response_type=code&client_id=${client_id}&redirect_uri=${window.location.origin}/oauth2/authorize&scope=${scope}`;
localStorage.setItem('sourceWaitingForOAuth', 'newSource');
localStorage.setItem('currentAppEnvironmentIdForOauth', currentAppEnvironmentId);
optionchanged('provider', provider).then(() => {

View file

@ -98,7 +98,7 @@ function setOauth2Token(dataSourceId, body, current_organization_id) {
function fetchOauth2BaseUrl(provider, plugin_id = null, source_options = {}) {
const payload = { provider, ...(plugin_id && { plugin_id }), ...(source_options && { source_options }) };
const requestOptions = {
method: 'GET',
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(payload),

View file

@ -9,6 +9,8 @@ export const globalDatasourceService = {
convertToGlobal,
getDataSourceByEnvironmentId,
getForApp,
getQueriesLinkedToDatasource,
getQueriesLinkedToMarketplacePlugin,
};
function getForApp(organizationId, appVersionId, environmentId) {
@ -68,3 +70,15 @@ function getDataSourceByEnvironmentId(dataSourceId, environmentId) {
handleResponse
);
}
function getQueriesLinkedToMarketplacePlugin(pluginId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/data-sources/dependent-queries/marketplace-plugin/${pluginId}`, requestOptions).then(
handleResponse
);
}
function getQueriesLinkedToDatasource(dataSourceId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/data-sources/dependent-queries/${dataSourceId}`, requestOptions).then(handleResponse);
}

View file

@ -12,6 +12,7 @@ export const userService = {
getAvatar,
updateAvatar,
updateUserType,
updateUserTypeInstance,
getUserLimits,
changeUserPassword,
generateUserPassword,
@ -80,6 +81,16 @@ function updateUserType(userUpdateBody) {
return fetch(`${config.apiUrl}/users/user-type`, requestOptions).then(handleResponse);
}
function updateUserTypeInstance(userUpdateBody) {
const requestOptions = {
method: 'PATCH',
headers: authHeader(),
body: JSON.stringify(userUpdateBody),
credentials: 'include',
};
return fetch(`${config.apiUrl}/users/user-type/instance`, requestOptions).then(handleResponse);
}
function changePassword(currentPassword, newPassword) {
const body = { currentPassword, newPassword };
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };

View file

@ -21,7 +21,7 @@ import config from 'config';
import { capitalize, isEmpty } from 'lodash';
import { Card } from '@/_ui/Card';
import { withTranslation, useTranslation } from 'react-i18next';
import { camelizeKeys, decamelizeKeys } from 'humps';
import { camelizeKeys, decamelizeKeys, decamelize } from 'humps';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { useAppVersionStore } from '@/_stores/appVersionStore';
@ -249,14 +249,26 @@ class DataSourceManagerComponent extends React.Component {
const scope = this.state?.scope || selectedDataSource?.scope;
const parsedOptions = Object?.keys(options)?.map((key) => {
const keyMeta = dataSourceMeta.options[key];
let keyMeta = dataSourceMeta.options[key];
let isEncrypted = false;
if (keyMeta) {
isEncrypted = keyMeta.encrypted;
}
// to resolve any casing mis-match
if (decamelize(key) !== key) {
const newKey = decamelize(key);
isEncrypted = dataSourceMeta.options[newKey]?.encrypted;
}
return {
key: key,
value: options[key].value,
encrypted: keyMeta ? keyMeta.encrypted : false,
encrypted: isEncrypted,
...(!options[key]?.value && { credential_id: options[key]?.credential_id }),
};
});
if (OAuthDs.includes(kind)) {
const value = localStorage.getItem('OAuthCode');
parsedOptions.push({ key: 'code', value, encrypted: false });

View file

@ -9,6 +9,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
import { SearchBox } from '@/_components/SearchBox';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton';
import Modal from '@/HomePage/Modal';
export const List = ({ updateSelectedDatasource }) => {
const {
@ -28,6 +29,7 @@ export const List = ({ updateSelectedDatasource }) => {
const [isDeleteModalVisible, setDeleteModalVisibility] = React.useState(false);
const [filteredData, setFilteredData] = useState(dataSources);
const [showInput, setShowInput] = useState(false);
const [showDependentQueriesInfo, setShowDependentQueriesInfo] = useState(false);
const darkMode = localStorage.getItem('darkMode') === 'true';
@ -50,7 +52,7 @@ export const List = ({ updateSelectedDatasource }) => {
setCurrentEnvironment(environments[0]);
toggleDataSourceManagerModal(true);
updateSelectedDatasource(selectedSource?.name);
setDeleteModalVisibility(true);
getQueriesLinkedToDatasource(selectedSource);
};
const executeDataSourceDeletion = () => {
@ -74,6 +76,21 @@ export const List = ({ updateSelectedDatasource }) => {
});
};
const getQueriesLinkedToDatasource = (selectedSource) => {
globalDatasourceService
.getQueriesLinkedToDatasource(selectedSource.id)
.then((data) => {
if (data?.dependent_queries) {
setShowDependentQueriesInfo(true);
} else {
setDeleteModalVisibility(true);
}
})
.catch(({ error }) => {
toast.error(error);
});
};
const cancelDeleteDataSource = () => {
setDeleteModalVisibility(false);
};
@ -171,6 +188,16 @@ export const List = ({ updateSelectedDatasource }) => {
)}
</div>
</div>
<Modal
title="Dependent queries found!"
show={showDependentQueriesInfo}
closeModal={() => setShowDependentQueriesInfo(false)}
>
<div className="mt-3 mb-3">
Cannot delete <b>{selectedDataSource?.name ? selectedDataSource.name : 'datasource'}</b> as it is used in the
apps
</div>
</Modal>
<ConfirmDialog
show={isDeleteModalVisible}
message={'You will lose all the queries created from this data source. Do you really want to delete?'}

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@
"@types/jest": "^29.5.0",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"aws-sdk": "^2.1326.0",
"aws-sdk": "^2.1692.0",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-jest": "^27.2.1",

View file

@ -0,0 +1,5 @@
node_modules
lib/*.d.*
lib/*.js
lib/*.js.map
dist/*

View file

@ -0,0 +1,4 @@
# ClickUp
Documentation on: https://docs.tooljet.com/docs/data-sources/clickup

View file

@ -0,0 +1,7 @@
'use strict';
const clickup = require('../lib');
describe('clickup', () => {
it.todo('needs tests');
});

View file

@ -0,0 +1,19 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_485_45)">
<path d="M4 16.5435L6.88198 14.3135C8.41315 16.332 10.0399 17.2624 11.8507 17.2624C13.6518 17.2624 15.2326 16.343 16.6947 14.3403L19.6179 16.5166C17.5081 19.4044 14.8864 20.9302 11.8507 20.9302C8.82468 20.9302 6.17753 19.4142 4 16.5435Z" fill="url(#paint0_linear_485_45)"/>
<path d="M11.8415 6.85133L6.71177 11.3163L4.34058 8.53855L11.8524 2.00004L19.3048 8.5434L16.9228 11.3114L11.8415 6.85133Z" fill="url(#paint1_linear_485_45)"/>
</g>
<defs>
<linearGradient id="paint0_linear_485_45" x1="4" y1="22.1219" x2="19.6179" y2="22.1219" gradientUnits="userSpaceOnUse">
<stop stop-color="#7612FA"/>
<stop offset="1" stop-color="#40DDFF"/>
</linearGradient>
<linearGradient id="paint1_linear_485_45" x1="4.34058" y1="12.9942" x2="19.3048" y2="12.9942" gradientUnits="userSpaceOnUse">
<stop stop-color="#FA12E3"/>
<stop offset="1" stop-color="#FFD700"/>
</linearGradient>
<clipPath id="clip0_485_45">
<rect width="16" height="20" fill="white" transform="translate(4 2)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,114 @@
import { QueryError, QueryResult, QueryService, ConnectionTestResult } from '@tooljet-marketplace/common';
import { SourceOptions } from './types';
import got, { Headers } from 'got';
export default class Clickup implements QueryService {
authHeader(token: string): Headers {
return { Authorization: token };
}
async run(sourceOptions: SourceOptions, queryOptions: any, dataSourceId: string): Promise<QueryResult> {
const operation = queryOptions.operation;
const apiKey = sourceOptions.apiKey;
const baseUrl = 'https://api.clickup.com/api';
const path = queryOptions['path'];
const pathParams = queryOptions['params']['path'];
const queryParams = queryOptions['params']['query'];
const bodyParams = queryOptions['params']['request'];
// Replace path params in URL
let modifiedPath = path;
for (const param of Object.keys(pathParams)) {
modifiedPath = modifiedPath.replace(`{${param}}`, pathParams[param]);
}
const url = `${baseUrl}${modifiedPath}`;
try {
let response;
if (operation === 'get' || operation === 'delete') {
response = await got(url, {
method: operation,
headers: this.authHeader(apiKey),
searchParams: queryParams,
});
} else {
// post, put, patch operations
const resolvedBodyParams = this.resolveBodyparams(bodyParams);
response = await got(url, {
method: operation,
headers: this.authHeader(apiKey),
json: resolvedBodyParams,
searchParams: queryParams,
});
}
return {
status: 'ok',
data: JSON.parse(response.body),
};
} catch (err) {
const errorMessage = err.message || 'An unknown error occurred';
const errorDetails: any = {};
if (err.response) {
const { statusCode, body } = err.response;
errorDetails.statusCode = statusCode;
try {
const parsedBody = JSON.parse(body);
errorDetails.error = parsedBody.err || null;
errorDetails.code = parsedBody.ECODE || null;
} catch (parseError) {
errorDetails.rawBody = body;
}
}
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
}
}
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
const apiKey = sourceOptions.apiKey;
try {
const response = await got('https://api.clickup.com/api/v2/user', {
headers: this.authHeader(apiKey),
});
const data = JSON.parse(response.body);
if (data?.user?.id) {
return {
status: 'ok',
};
} else {
throw new QueryError('User information not found', 'Invalid API key or insufficient permissions', {});
}
} catch (error) {
throw new QueryError('Connection could not be established', error.response?.body || error.message, {});
}
}
private resolveBodyparams(bodyParams: object): object {
if (typeof bodyParams === 'string') {
return bodyParams;
}
const expectedResult = {};
for (const key of Object.keys(bodyParams)) {
if (typeof bodyParams[key] === 'object') {
for (const subKey of Object.keys(bodyParams[key])) {
expectedResult[`${key}[${subKey}]`] = bodyParams[key][subKey];
}
} else {
expectedResult[key] = bodyParams[key];
}
}
return expectedResult;
}
}

View file

@ -0,0 +1,33 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
"title": "ClickUp datasource",
"description": "Clickup plugin for task, list, and doc management",
"type": "api",
"source": {
"name": "ClickUp",
"kind": "clickup",
"exposedVariables": {
"isLoading": false,
"data": {},
"rawData": {}
},
"options": {
"apiKey": {
"type": "string",
"encrypted": true
}
}
},
"defaults": {},
"properties": {
"apiKey": {
"label": "API Key",
"key": "apiKey",
"type": "password",
"description": "Enter your Personal API Token"
}
},
"required": [
"apiKey"
]
}

View file

@ -0,0 +1,16 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "ClickUp datasource",
"description": "A schema defining ClickUp datasource",
"type": "api",
"defaults": {},
"properties": {
"operation": {
"label": "",
"key": "clickup_operation",
"type": "react-component-api-endpoint",
"description": "Single select dropdown for operation",
"spec_url": "https://developer.clickup.com/openapi/673cf4cfdca96a0019533cad"
}
}
}

View file

@ -0,0 +1,3 @@
export type SourceOptions = {
apiKey: string;
};

View file

@ -0,0 +1,26 @@
{
"name": "@tooljet-marketplace/clickup",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"build": "ncc build lib/index.ts -o dist",
"watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
"@tooljet-marketplace/common": "^1.0.0"
},
"devDependencies": {
"typescript": "^4.7.4",
"@vercel/ncc": "^0.34.0"
}
}

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "lib"
},
"exclude": [
"node_modules",
"dist"
]
}

View file

@ -15,6 +15,9 @@
"options": {
"url": {
"type": "string"
},
"personal_token": {
"encrypted": true
}
}
},
@ -57,11 +60,12 @@
"key": "personal_token",
"type": "password",
"description": "Enter your api token",
"hint": "You can generate a personal access token from your Jira account 'Manage account'."
"hint": "You can generate a personal access token from your Jira account 'Manage account'.",
"encrypted": true
}
}
},
"required": [
"url"
]
}
}

View file

@ -146,7 +146,7 @@
"height": "36px"
},
"withPayload": {
"label": "Include metadata",
"label": "Include payload",
"key": "withPayload",
"type": "codehinter",
"description": "Whether to return payload values.",
@ -163,4 +163,4 @@
}
}
}
}
}

View file

@ -69,7 +69,16 @@ export default class Supabase implements QueryService {
}
if (error) {
throw new QueryError('Query could not be completed', error, {});
const errorMessage = error?.message || "An unknown error occurred.";
let errorDetails: any = {};
const supabaseError = error as any;
const { code, hint } = supabaseError;
errorDetails.code = code;
errorDetails.hint = hint;
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
}
return {

View file

@ -3,6 +3,7 @@ import readDir from 'recursive-readdir';
import { resolve as _resolve } from 'path';
import aws from 'aws-sdk';
import { lookup } from 'mime-types';
import chalk from 'chalk';
const { config, S3 } = aws;
const __dirname = _resolve();
@ -30,7 +31,16 @@ const generateFileKey = (fileName) => {
const s3 = new S3();
const uploadToS3 = async () => {
const start = Date.now();
const errors = [];
let successCount = 0;
console.log(chalk.cyanBright('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
console.log(chalk.cyanBright('📤 S3 ASSETS UPLOADER'));
console.log(chalk.cyanBright('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
try {
console.log(`[${new Date().toLocaleTimeString()}] Scanning directory for files...`);
const fileArray = await getDirectoryFilesRecursive(directoryPath, [
'common',
'.DS_Store',
@ -43,30 +53,76 @@ const uploadToS3 = async () => {
'tsconfig.json',
]);
fileArray.map((file) => {
// Configuring parameters for S3 Object
const S3params = {
Bucket: process.env.AWS_BUCKET,
Body: createReadStream(file),
Key: generateFileKey(file),
ContentType: lookup(file),
ContentEncoding: 'utf-8',
CacheControl: 'immutable,max-age=31536000,public',
};
s3.upload(S3params, function (err, data) {
if (err) {
// Set the exit code while letting
// the process exit gracefully.
console.error(err);
process.exitCode = 1;
} else {
console.log(`Assets uploaded to S3: `, data);
}
console.log(`[${new Date().toLocaleTimeString()}] Found ${fileArray.length} files to upload`);
console.log(`[${new Date().toLocaleTimeString()}] Target bucket: ${process.env.AWS_BUCKET}\n`);
const uploadPromises = fileArray.map((file, index) => {
return new Promise((resolve) => {
const S3params = {
Bucket: process.env.AWS_BUCKET,
Body: createReadStream(file),
Key: generateFileKey(file),
ContentType: lookup(file) || 'application/octet-stream',
ContentEncoding: 'utf-8',
CacheControl: 'immutable,max-age=31536000,public',
};
s3.upload(S3params, function (err, data) {
const indexStr = `[${(index + 1).toString().padStart(2, '0')}/${fileArray.length}]`;
if (err) {
console.log(chalk.redBright(`${indexStr} ❌ Failed to upload: ${file}`));
console.error(chalk.gray(`${err.message}`));
errors.push({ file, message: err.message });
} else {
console.log(chalk.greenBright(`${indexStr} ✅ Uploaded: ${file}`));
console.log(
chalk.gray(
JSON.stringify(
{
ETag: data.ETag,
Location: data.Location,
Key: data.Key,
Bucket: data.Bucket,
},
null,
2
)
)
);
successCount++;
}
resolve();
});
});
});
await Promise.all(uploadPromises);
const duration = ((Date.now() - start) / 1000).toFixed(1);
console.log(chalk.cyanBright('\n━━━━━━━━━━━━━━━ UPLOAD SUMMARY ━━━━━━━━━━━━━━━━━'));
if (errors.length > 0) {
console.log(`[${new Date().toLocaleTimeString()}] ⚠️ Upload completed with ${errors.length} error(s)`);
} else {
console.log(`[${new Date().toLocaleTimeString()}] 🎉 All files uploaded successfully`);
}
console.log(`[${new Date().toLocaleTimeString()}] ✅ Successfully uploaded: ${successCount}/${fileArray.length} files`);
console.log(`[${new Date().toLocaleTimeString()}] ❌ Failed uploads: ${errors.length}/${fileArray.length} files`);
console.log(`[${new Date().toLocaleTimeString()}] Total time: ${duration}s`);
if (errors.length > 0) {
console.log(chalk.cyanBright('\n━━━━━━━━━━━━━━━ ERROR DETAILS ━━━━━━━━━━━━━━━━━'));
errors.forEach((err, idx) => {
console.log(chalk.red(`Error #${idx + 1}: ${err.file}`));
console.log(chalk.gray(`${err.message}`));
});
process.exitCode = 1;
}
} catch (error) {
console.error(chalk.bgRed.white('❌ Script failed with error:'));
console.error(error);
process.exit(1);
}
};
uploadToS3();
uploadToS3();

View file

@ -57,7 +57,18 @@ export default class FirestoreQueryService implements QueryService {
break;
}
} catch (error) {
throw new QueryError('Query could not be completed', error.message, {});
const errorMessage = error.message || "An unknown error occurred.";
let errorDetails: any = {};
if (error && error instanceof Error) {
const firestoreError = error as any;
const { code, name } = firestoreError;
errorDetails.code = code as string;
errorDetails.name = name;
}
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
}
return {

View file

@ -46,8 +46,17 @@ export default class GRPC implements QueryService {
metadata.add(sourceOptions.grpc_apikey_key, sourceOptions.grpc_apikey_value);
}
let jsonMessage = {};
if (queryOptions.jsonMessage) {
try {
jsonMessage = JSON.parse(queryOptions.jsonMessage);
} catch (e) {
throw new QueryError('Invalid JSON message', {}, {});
}
}
const result = await new Promise((resolve, reject) => {
clientStub[rpc]({}, metadata, (err: any, response: any) => {
clientStub[rpc](jsonMessage, metadata, (err: any, response: any) => {
if (err) {
reject(err);
}

View file

@ -11,5 +11,6 @@ export type SourceOptions = {
export type QueryOptions = {
operation: string;
serviceName: string;
jsonMessage: string;
rpc: string;
};

View file

@ -19,7 +19,8 @@
"type": "string"
},
"password": {
"type": "string"
"type": "string",
"encrypted": true
},
"connectionLimit": {
"type": "string"
@ -83,7 +84,8 @@
"label": "Password",
"key": "password",
"type": "password",
"description": "Enter password"
"description": "Enter password",
"encrypted": true
},
"connectionLimit": {
"label": "Connection Limit",

View file

@ -1,16 +1,16 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "Stripe datasource",
"description": "A schema defining stripe datasource",
"type": "api",
"defaults": {},
"properties": {
"operation": {
"label": "",
"key": "stripe_operation",
"type": "react-component-api-endpoint",
"description": "Single select dropdown for operation",
"specUrl": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json"
}
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "Stripe datasource",
"description": "A schema defining stripe datasource",
"type": "api",
"defaults": {},
"properties": {
"operation": {
"label": "",
"key": "stripe_operation",
"type": "react-component-api-endpoint",
"description": "Single select dropdown for operation",
"spec_url": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json"
}
}
}

View file

@ -1,5 +1,4 @@
node_modules
lib/*.d.*
lib/*.js
lib/*.js.map
lib/operations.json
lib/*.js.map

View file

@ -0,0 +1,105 @@
{
"title": "Woocommerce datasource",
"description": "A schema defining Woocommerce datasource",
"type": "api",
"defaults": {},
"properties": {
"resource": {
"label": "Resource",
"key": "resource",
"className": "col-md-4",
"type": "dropdown-component-flip",
"description": "Resource select",
"list": [
{ "value": "product", "name": "Product" },
{ "value": "customer", "name": "Customer" },
{ "value": "order", "name": "Order" },
{ "value": "coupon", "name": "Coupon" }
]
},
"customer": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"description": "Single select dropdown for operation",
"list": [
{ "value": "list_customer", "name": "List all customers" },
{ "value": "update_customer", "name": "Update a customer" },
{ "value": "delete_customer", "name": "Delete a customer" },
{ "value": "batch_update_customer", "name": "Batch update customers" },
{ "value": "create_customer", "name": "Create a customer" },
{ "value": "retrieve_customer", "name": "Retrieve a customer" }
]
},
"list_customer": {
"page": {
"label": "Page",
"key": "page",
"type": "codehinter",
"description": "Enter page",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "",
"lineNumbers": false
},
"context": {
"label": "Context",
"key": "context",
"type": "codehinter",
"description": "Enter context",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "",
"lineNumbers": false
}
}
},
"product": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"description": "Single select dropdown for operation",
"list": [
{ "value": "list_product", "name": "List all products" },
{ "value": "update_product", "name": "Update a product" },
{ "value": "delete_product", "name": "Delete a product" },
{ "value": "batch_update_product", "name": "Batch update products" },
{ "value": "create_product", "name": "Create a product" },
{ "value": "retrieve_product", "name": "Retrieve a product" }
]
}
},
"order": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"description": "Single select dropdown for operation",
"list": [
{ "value": "list_order", "name": "List all orders" },
{ "value": "update_order", "name": "Update an order" },
{ "value": "delete_order", "name": "Delete an order" },
{ "value": "batch_update_order", "name": "Batch update orders" },
{ "value": "create_order", "name": "Create an order" },
{ "value": "retrieve_order", "name": "Retrieve an order" }
]
}
},
"coupon": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"description": "Single select dropdown for operation",
"list": [
{ "value": "list_coupon", "name": "List all coupons" },
{ "value": "create_coupon", "name": "Create a coupon" }
]
}
}
}
}

View file

@ -1 +1 @@
3.13.0
3.14.0

@ -1 +1 @@
Subproject commit 30dbfa754562d00f8d64181d5006e113798bd668
Subproject commit f70ac83c38e0a8b44aeb2a0fb2059690eb5e2f46

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddResourceDataAudit1746520805456 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'audit_logs',
new TableColumn({
name: 'resource_data',
type: 'json',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner, TableUnique } from "typeorm";
export class AddPluginIdUniqueConstraint1747133448781 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createUniqueConstraint(
"plugins",
new TableUnique({
name: "UQ_plugin_pluginId",
columnNames: ["plugin_id"],
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropUniqueConstraint(
"plugins",
"UQ_plugin_pluginId"
);
}
}

View file

@ -1,6 +1,8 @@
#!/bin/bash
set -e
redis-server /etc/redis/redis.conf &
# Fix ownership and permissions
chown -R postgres:postgres /var/lib/postgresql /var/run/postgresql
chmod 0700 /var/lib/postgresql/13/main

View file

@ -221,5 +221,13 @@
"id": "azurerepos",
"author": "Tooljet",
"timestamp": "Mon, 23 Dec 2024 11:57:30 GMT"
},
{
"name": "ClickUp",
"description": "ClickUp plugin for task, list, and doc management",
"version": "1.0.0",
"id": "clickup",
"author": "Tooljet",
"timestamp": "Wed, 16 Apr 2025 15:31:38 GMT"
}
]

View file

@ -23,6 +23,9 @@ export class AuditLog extends BaseEntity {
@Column({ name: 'resource_type', type: 'enum', enum: MODULES })
resourceType: MODULES;
@Column('simple-json', { name: 'resource_data' })
resourceData;
@Column({ name: 'action_type' })
actionType: string;

View file

@ -6,9 +6,11 @@ import {
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
Unique
} from 'typeorm';
import { File } from 'src/entities/file.entity';
@Unique(['pluginId'])
@Entity({ name: 'plugins' })
export class Plugin {
@PrimaryGeneratedColumn()

View file

@ -7,6 +7,6 @@ export interface IAuditLogService {
perform(
{ userId, organizationId, resourceId, resourceType, actionType, resourceName, metadata }: AuditLogFields,
manager?: EntityManager
): Promise<AuditLog>;
): Promise<AuditLog[]>;
findPerPage(user: User, query: AuditLogsQuery): Promise<any>;
}

View file

@ -18,10 +18,12 @@ export interface AuditLogFields {
organizationId: string;
resourceId: string;
resourceType: MODULES;
resourceData?: object;
actionType: string;
resourceName?: string;
ipAddress?: string;
metadata?: object;
organizationIds?: Array<string>;
}
export interface Features {

View file

@ -28,12 +28,15 @@ export const FEATURES: FeaturesConfig = {
},
[FEATURE_KEY.FORGOT_PASSWORD]: {
isPublic: true,
auditLogsKey: 'USER_PASSWORD_FORGOT',
},
[FEATURE_KEY.RESET_PASSWORD]: {
isPublic: true,
auditLogsKey: 'USER_PASSWORD_RESET',
},
[FEATURE_KEY.OAUTH_SIGN_IN]: {
isPublic: true,
auditLogsKey: 'USER_LOGIN',
},
[FEATURE_KEY.OAUTH_OPENID_CONFIGS]: {
isPublic: true,
@ -43,6 +46,7 @@ export const FEATURES: FeaturesConfig = {
},
[FEATURE_KEY.OAUTH_COMMON_SIGN_IN]: {
isPublic: true,
auditLogsKey: 'USER_LOGIN',
},
[FEATURE_KEY.OAUTH_SAML_RESPONSE]: {
isPublic: true,

View file

@ -124,6 +124,9 @@ export class AuthService implements IAuthService {
organizationId: organization.id,
resourceId: user.id,
resourceName: user.email,
resourceData: {
auth_method: 'password',
},
});
}
@ -184,6 +187,13 @@ export class AuthService implements IAuthService {
forgotPasswordToken: null,
passwordRetryCount: 0,
});
const auditLogEntry = {
userId: user.id,
organizationId: user.defaultOrganizationId,
resourceId: user.id,
resourceName: user.email,
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
}
}
@ -195,6 +205,13 @@ export class AuthService implements IAuthService {
}
const forgotPasswordToken = uuid.v4();
await this.userRepository.updateOne(user.id, { forgotPasswordToken });
const auditLogEntry = {
userId: user.id,
organizationId: user.defaultOrganizationId,
resourceId: user.id,
resourceName: user.email,
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
this.eventEmitter.emit('emailEvent', {
type: EMAIL_EVENTS.SEND_PASSWORD_RESET_EMAIL,
payload: {

View file

@ -5,6 +5,7 @@ import { UserAllPermissions } from '@modules/app/types';
import { FEATURE_KEY } from '../constants';
import { DataSource } from '@entities/data_source.entity';
import { MODULES } from '@modules/app/constants/modules';
import { getTooljetEdition } from '@helpers/utils.helper';
type Subjects = InferSubjects<typeof DataSource> | 'all';
export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>;
@ -33,10 +34,13 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
const isAllViewable = !!resourcePermissions?.isAllUsable;
const dataSourceId = request?.tj_resource_id;
const toolJetEdition = getTooljetEdition();
// Oauth end points available to all
can(FEATURE_KEY.GET_OAUTH2_BASE_URL, DataSource);
can(FEATURE_KEY.AUTHORIZE, DataSource);
if ((toolJetEdition == 'ee' && superAdmin) || (toolJetEdition !== 'ee' && isAdmin)) {
can(FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN, DataSource);
}
if (isBuilder) {
// Only builder can do scope change, Get call is there on app builder
@ -56,6 +60,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
FEATURE_KEY.TEST_CONNECTION,
FEATURE_KEY.SCOPE_CHANGE,
FEATURE_KEY.GET_FOR_APP,
FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE,
],
DataSource
);
@ -70,7 +75,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
);
if (isCanDelete) {
can(FEATURE_KEY.DELETE, DataSource);
can([FEATURE_KEY.DELETE, FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE], DataSource);
}
if (isCanCreate) {
can(FEATURE_KEY.CREATE, DataSource);

View file

@ -20,5 +20,7 @@ export const FEATURES: FeaturesConfig = {
[FEATURE_KEY.GET_OAUTH2_BASE_URL]: {},
[FEATURE_KEY.AUTHORIZE]: {},
[FEATURE_KEY.GET_FOR_APP]: {},
[FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE]: {},
[FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN]: {},
},
};

View file

@ -9,6 +9,8 @@ export enum FEATURE_KEY {
TEST_CONNECTION = 'TEST_CONNECTION',
GET_OAUTH2_BASE_URL = 'GET_OAUTH2_BASE_URL',
AUTHORIZE = 'AUTHORIZE',
QUERIES_LINKED_TO_DATASOURCE = 'QUERIES_LINKED_TO_DATASOURCE',
QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN = 'QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN',
}
export enum DataSourceTypes {

View file

@ -111,7 +111,7 @@ export class DataSourcesController implements IDataSourcesController {
@InitFeature(FEATURE_KEY.GET_OAUTH2_BASE_URL)
@UseGuards(FeatureAbilityGuard)
@Get('fetch-oauth2-base-url')
@Post('fetch-oauth2-base-url')
getAuthUrl(@Body() getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto) {
return this.dataSourcesService.getAuthUrl(getDataSourceOauthUrlDto);
}
@ -129,6 +129,20 @@ export class DataSourcesController implements IDataSourcesController {
return;
}
@InitFeature(FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN)
@UseGuards(FeatureAbilityGuard)
@Get('dependent-queries/marketplace-plugin/:plugin_id')
async findDatasourcesAndQueriesOfMarketplacePlugin(@User() user: UserEntity, @Param('plugin_id') pluginId) {
return await this.dataSourcesService.findDatasourcesAndQueriesOfMarketplacePlugin(pluginId);
}
@InitFeature(FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE)
@UseGuards(FeatureAbilityGuard)
@Get('dependent-queries/:datasource_id')
async findQueriesLinkedToDatasource(@User() user: UserEntity, @Param('datasource_id') datasourceId: string) {
return await this.dataSourcesService.findQueriesLinkedToDatasource(datasourceId);
}
@InitFeature(FEATURE_KEY.AUTHORIZE)
@UseGuards(FeatureAbilityGuard)
@Post('decrypt')

View file

@ -10,6 +10,7 @@ import { InstanceSettingsModule } from '@modules/instance-settings/module';
import { VersionRepository } from '@modules/versions/repository';
import { AppsRepository } from '@modules/apps/repository';
import { TooljetDbModule } from '@modules/tooljet-db/module';
import { OrganizationRepository } from '@modules/organizations/repository';
import { SessionModule } from '@modules/session/module';
import { SampleDBScheduler } from './schedulers/sample-db.scheduler';
@ -21,6 +22,7 @@ export class DataSourcesModule {
const { DataSourcesUtilService } = await import(`${importPath}/data-sources/util.service`);
const { PluginsServiceSelector } = await import(`${importPath}/data-sources/services/plugin-selector.service`);
const { SampleDataSourceService } = await import(`${importPath}/data-sources/services/sample-ds.service`);
const { OrganizationsService } = await import(`${importPath}/organizations/service`);
return {
module: DataSourcesModule,
@ -42,6 +44,8 @@ export class DataSourcesModule {
PluginsRepository,
SampleDataSourceService,
FeatureAbilityFactory,
OrganizationsService,
OrganizationRepository,
SampleDBScheduler,
],
controllers: [DataSourcesController],

View file

@ -168,4 +168,26 @@ export class DataSourcesRepository extends Repository<DataSource> {
});
}, manager || this.manager);
}
getDatasourceByPluginId(pluginId: string) {
return dbTransactionWrap((manager: EntityManager) => {
return manager.find(DataSource, {
where: {
pluginId: pluginId,
},
relations: ['dataQueries'],
});
});
}
getQueriesByDatasourceId(datasourceId) {
return dbTransactionWrap((manager: EntityManager) => {
return manager.find(DataSource, {
where: {
id: datasourceId,
},
relations: ['dataQueries'],
});
});
}
}

View file

@ -20,6 +20,8 @@ import { GetQueryVariables, UpdateOptions } from './types';
import { DataSource } from '@entities/data_source.entity';
import { PluginsServiceSelector } from './services/plugin-selector.service';
import { IDataSourcesService } from './interfaces/IService';
// import { FEATURE_KEY } from './constants';
import { OrganizationsService } from '@modules/organizations/service';
import { RequestContext } from '@modules/request-context/service';
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
@ -30,7 +32,8 @@ export class DataSourcesService implements IDataSourcesService {
protected readonly dataSourcesUtilService: DataSourcesUtilService,
protected readonly abilityService: AbilityService,
protected readonly appEnvironmentsUtilService: AppEnvironmentUtilService,
protected readonly pluginsServiceSelector: PluginsServiceSelector
protected readonly pluginsServiceSelector: PluginsServiceSelector,
protected readonly organizationsService: OrganizationsService
) {}
async getForApp(query: GetQueryVariables, user: User): Promise<{ data_sources: object[] }> {
@ -43,7 +46,6 @@ export class DataSourcesService implements IDataSourcesService {
const dataSources = await this.dataSourcesRepository.allGlobalDS(userPermissions, user.organizationId, query ?? {});
let staticDataSources = await this.dataSourcesRepository.getAllStaticDataSources(query.appVersionId);
if (!shouldIncludeWorkflows) {
// remove workflowsdefault data source from static data sources
staticDataSources = staticDataSources.filter((dataSource) => dataSource.kind !== 'workflows');
@ -176,6 +178,12 @@ export class DataSourcesService implements IDataSourcesService {
if (dataSource.type === DataSourceTypes.SAMPLE) {
throw new BadRequestException('Cannot delete sample data source');
}
const result = await this.findQueriesLinkedToDatasource(dataSourceId);
if (result.dependent_queries) {
throw new BadRequestException(`Datasource can't be deleted, queries are in use`);
}
await this.dataSourcesRepository.delete(dataSourceId);
// Setting data for audit logs
@ -243,4 +251,30 @@ export class DataSourcesService implements IDataSourcesService {
await this.dataSourcesUtilService.authorizeOauth2(dataSource, code, user.id, environmentId, user.organizationId);
return;
}
async findQueriesLinkedToDatasource(datasourceId: string) {
const dataSourceDetails = await this.dataSourcesRepository.getQueriesByDatasourceId(datasourceId);
if (dataSourceDetails.length == 0) return { datasources: 0, dependent_queries: 0 };
const queries = [];
dataSourceDetails.forEach((datasourceDetail) => {
const { dataQueries = [] } = datasourceDetail;
if (dataQueries.length) queries.push(...dataQueries);
});
return { datasources: dataSourceDetails.length, dependent_queries: queries.length };
}
async findDatasourcesAndQueriesOfMarketplacePlugin(pluginId: string) {
const dataSourcesByMarketplacePlugin = await this.dataSourcesRepository.getDatasourceByPluginId(pluginId);
if (!dataSourcesByMarketplacePlugin.length) return { dependent_queries: 0 };
const queries = [];
dataSourcesByMarketplacePlugin?.forEach((datasource) => {
if (datasource.dataQueries.length) queries.push(...datasource.dataQueries);
});
return {
dependent_queries: queries.length,
};
}
}

View file

@ -14,6 +14,8 @@ interface Features {
[FEATURE_KEY.GET_OAUTH2_BASE_URL]: FeatureConfig;
[FEATURE_KEY.AUTHORIZE]: FeatureConfig;
[FEATURE_KEY.GET_FOR_APP]: FeatureConfig;
[FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE]: FeatureConfig;
[FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN]: FeatureConfig;
}
export interface FeaturesConfig {

View file

@ -189,17 +189,16 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
/*
Basic plan customer. lets update all environment options.
this will help us to run the queries successfully when the user buys enterprise plan
*/
await Promise.all(
allEnvs.map(async (envToUpdate) => {
dataSource.options = (
await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, envToUpdate.id)
).options;
*/
const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
await this.appEnvironmentUtilService.updateOptions(newOptions, envToUpdate.id, dataSource.id, manager);
})
);
const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
for (const env of allEnvs) {
dataSource.options = (
await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, env.id)
).options;
await this.appEnvironmentUtilService.updateOptions(newOptions, env.id, dataSource.id, manager);
}
}
const updatableParams = {
id: dataSourceId,

View file

@ -6,6 +6,7 @@ export const FEATURES: FeaturesConfig = {
[MODULES.ONBOARDING]: {
[FEATURE_KEY.ACTIVATE_ACCOUNT]: {
isPublic: true,
auditLogsKey: 'USER_SIGNUP',
}, // Account Activation
[FEATURE_KEY.SETUP_SUPER_ADMIN]: {
isPublic: true,
@ -15,6 +16,7 @@ export const FEATURES: FeaturesConfig = {
}, // Signup
[FEATURE_KEY.ACCEPT_INVITE]: {
isPublic: true,
auditLogsKey: 'USER_INVITE_REDEEM',
}, // Accept Invitation
[FEATURE_KEY.RESEND_INVITE]: {
isPublic: true,
@ -27,6 +29,7 @@ export const FEATURES: FeaturesConfig = {
}, // Verify Organization Token
[FEATURE_KEY.SETUP_ACCOUNT_FROM_TOKEN]: {
isPublic: true,
auditLogsKey: 'USER_SIGNUP',
}, // Setup Account From Token
[FEATURE_KEY.CHECK_WORKSPACE_UNIQUENESS]: {
isPublic: true,

View file

@ -120,7 +120,7 @@ export class OnboardingService implements IOnboardingService {
const userParams = { email, password, firstName, lastName };
// Find the default workspace
const defaultWorkspace = await this.organizationRepository. getDefaultWorkspaceOfInstance();
const defaultWorkspace = await this.organizationRepository.getDefaultWorkspaceOfInstance();
if (existingUser) {
// Handling instance and workspace level signup for existing user
@ -133,7 +133,7 @@ export class OnboardingService implements IOnboardingService {
manager
);
} else {
if(defaultWorkspace && !signingUpOrganization) {
if (defaultWorkspace && !signingUpOrganization) {
return await this.onboardingUtilService.createUserInDefaultWorkspace(
userParams,
defaultWorkspace,
@ -263,7 +263,8 @@ export class OnboardingService implements IOnboardingService {
throw new BadRequestException('Please enter password');
}
const activateDefaultWorkspace = (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
const activateDefaultWorkspace =
(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
if (activateDefaultWorkspace) {
// Getting default workspace
const defaultOrganizationUser: OrganizationUser = user.organizationUsers.find(
@ -277,11 +278,11 @@ export class OnboardingService implements IOnboardingService {
// Activate default workspace
await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager);
if(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId){
if (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) {
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(user.id);
for(const personalWorkspace of personalWorkspaces){
for (const personalWorkspace of personalWorkspaces) {
// if any personal workspace left. activate those
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
}
}
@ -362,6 +363,9 @@ export class OnboardingService implements IOnboardingService {
organizationId: organization?.id,
resourceId: user.id,
resourceName: user.email,
resourceData: {
signup_method: 'self-signup',
},
});
await this.licenseUserService.validateUser(manager);
@ -421,6 +425,13 @@ export class OnboardingService implements IOnboardingService {
}
const isWorkspaceSignup = organizationUser.source === WORKSPACE_USER_SOURCE.SIGNUP;
await this.licenseUserService.validateUser(manager);
const auditLogEntry = {
userId: user.id,
organizationId: organization.id,
resourceId: user.id,
resourceName: user.email,
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
return this.sessionUtilService.generateLoginResultPayload(
response,
user,
@ -534,6 +545,16 @@ export class OnboardingService implements IOnboardingService {
Till now user doesn't have an organization.
*/
await this.licenseUserService.validateUser(manager);
const auditLogsData = {
userId: signupUser.id,
organizationId: signupUser.organizationUsers[0].organizationId,
resourceId: signupUser.id,
resourceName: signupUser.email,
resourceData: {
signup_method: 'invite-redemption',
},
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogsData);
return this.onboardingUtilService.processOrganizationSignup(
response,
signupUser,
@ -566,7 +587,6 @@ export class OnboardingService implements IOnboardingService {
if (user.status !== USER_STATUS.ACTIVE) {
throw new BadRequestException(getUserErrorMessages(user.status));
}
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: organizationUser.organizationId,

View file

@ -6,12 +6,27 @@ export const FEATURES: FeaturesConfig = {
[MODULES.ORGANIZATION_USER]: {
[FEATURE_KEY.SUGGEST_USERS]: {},
[FEATURE_KEY.VIEW_ALL_USERS]: {},
[FEATURE_KEY.USER_ARCHIVE_ALL]: {},
[FEATURE_KEY.USER_ARCHIVE]: {},
[FEATURE_KEY.USER_INVITE]: {},
[FEATURE_KEY.USER_ARCHIVE_ALL]: {
isPublic: true,
auditLogsKey: 'USER_ARCHIVE',
},
[FEATURE_KEY.USER_ARCHIVE]: {
isPublic: true,
auditLogsKey: 'USER_ARCHIVE',
},
[FEATURE_KEY.USER_INVITE]: {
isPublic: true,
auditLogsKey: 'USER_INVITE',
},
[FEATURE_KEY.USER_BULK_UPLOAD]: {},
[FEATURE_KEY.USER_UNARCHIVE]: {},
[FEATURE_KEY.USER_UNARCHIVE_ALL]: {},
[FEATURE_KEY.USER_UNARCHIVE]: {
isPublic: true,
auditLogsKey: 'USER_UNARCHIVE',
},
[FEATURE_KEY.USER_UNARCHIVE_ALL]: {
isPublic: true,
auditLogsKey: 'USER_UNARCHIVE',
},
[FEATURE_KEY.USER_UPDATE]: {},
},
};

View file

@ -90,14 +90,14 @@ export class OrganizationUsersController implements IOrganizationUsersController
if (user.id === userId) {
throw new NotAcceptableException('Self archive not allowed');
}
await this.organizationUsersService.archiveFromAll(userId);
await this.organizationUsersService.archiveFromAll(userId, user);
return;
}
@InitFeature(FEATURE_KEY.USER_UNARCHIVE_ALL)
@Post(':userId/unarchive-all')
async unarchiveAll(@User() user: UserEntity, @Param('userId') userId: string) {
await this.organizationUsersService.unarchiveUser(userId);
await this.organizationUsersService.unarchiveUser(userId, user);
return;
}

View file

@ -6,8 +6,8 @@ import { UpdateOrgUserDto } from '../dto';
export interface IOrganizationUsersService {
updateOrgUser(organizationUserId: string, user: User, updateOrgUserDto: UpdateOrgUserDto): Promise<void>;
archive(id: string, organizationId: string, user?: User): Promise<void>;
archiveFromAll(userId: string): Promise<void>;
unarchiveUser(userId: string): Promise<void>;
archiveFromAll(userId: string, user: User): Promise<void>;
unarchiveUser(userId: string, user: User): Promise<void>;
unarchive(user: User, id: string, organizationId: string): Promise<void>;
inviteNewUser(currentUser: User, inviteNewUserDto: InviteNewUserDto): Promise<void>;
bulkUploadUsers(currentUser: User, fileStream: any, res: Response): Promise<void>;

View file

@ -24,6 +24,9 @@ import { Response } from 'express';
import { UserCsvRow } from './interfaces';
import { IOrganizationUsersService } from './interfaces/IService';
import { UpdateOrgUserDto } from './dto';
import { RequestContext } from '@modules/request-context/service';
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
import { Organization } from '@entities/organization.entity';
@Injectable()
export class OrganizationUsersService implements IOrganizationUsersService {
constructor(
@ -38,7 +41,6 @@ export class OrganizationUsersService implements IOrganizationUsersService {
async updateOrgUser(organizationUserId: string, user: User, updateOrgUserDto: UpdateOrgUserDto) {
const { firstName, lastName, addGroups, role, userMetadata } = updateOrgUserDto;
const organizationUser = await this.organizationUsersRepository.findOne({
where: { id: organizationUserId, organizationId: user.organizationId },
});
@ -81,35 +83,84 @@ export class OrganizationUsersService implements IOrganizationUsersService {
}
async archive(id: string, organizationId: string, user?: User): Promise<void> {
const organizationUser = await this.organizationUsersRepository.findOneOrFail({
where: { id, organizationId },
relations: ['user'],
});
await dbTransactionWrap(async (manager: EntityManager) => {
const organizationUser = await manager.findOneOrFail(OrganizationUser, {
where: { id, organizationId },
relations: ['user'],
});
await this.organizationUsersUtilService.throwErrorIfUserIsLastActiveAdmin(organizationUser?.user, organizationId);
await this.organizationUsersRepository.update(id, {
status: WORKSPACE_USER_STATUS.ARCHIVED,
invitationToken: null,
await this.organizationUsersUtilService.throwErrorIfUserIsLastActiveAdmin(organizationUser?.user, organizationId);
await manager.update(OrganizationUser, id, {
status: WORKSPACE_USER_STATUS.ARCHIVED,
invitationToken: null,
});
const organization = await manager.findOne(Organization, {
where: { id: organizationUser.organizationId },
});
const auditLogEntry = {
userId: user.id,
organizationId: user.defaultOrganizationId,
resourceId: user.id,
resourceName: organizationUser.user.email,
resourceData: {
archived_user: {
id: organizationUser.userId,
email: organizationUser.user.email,
first_name: organizationUser.user.firstName,
last_name: organizationUser.user.lastName,
},
archived_user_workspace: {
workspace_name: organization.name,
workspace_id: organization.id,
},
},
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
});
}
async archiveFromAll(userId: string): Promise<void> {
async archiveFromAll(userId: string, user: User): Promise<void> {
await dbTransactionWrap(async (manager: EntityManager) => {
const archivedUserWorkspaces = await manager.find(OrganizationUser, {
where: { userId },
relations: ['user'],
});
await manager.update(
OrganizationUser,
{ userId },
{ status: WORKSPACE_USER_STATUS.ARCHIVED, invitationToken: null }
);
await this.organizationUsersUtilService.updateUserStatus(userId, USER_STATUS.ARCHIVED, manager);
const organizationIds = archivedUserWorkspaces.map((user) => user.organizationId);
const auditLogEntry = {
userId: user.id,
organizationIds: organizationIds,
resourceId: user.id,
resourceName: archivedUserWorkspaces[0].user.email,
resourceData: {
archived_user: {
id: archivedUserWorkspaces[0].userId,
email: archivedUserWorkspaces[0].user.email,
first_name: archivedUserWorkspaces[0].user.firstName,
last_name: archivedUserWorkspaces[0].user.lastName,
},
},
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
});
}
async unarchiveUser(userId: string): Promise<void> {
async unarchiveUser(userId: string, user: User): Promise<void> {
await dbTransactionWrap(async (manager: EntityManager) => {
const targetUser = await manager.findOneOrFail(User, {
where: { id: userId },
select: ['id', 'status', 'invitationToken', 'source'],
});
const unarchivedUserWorkspaces = await manager.find(OrganizationUser, {
where: { userId },
relations: ['user'],
});
const { status, invitationToken } = targetUser;
/* Special case. what if the user is archived when the status is invited. we were changing status to active before */
const updatedStatus =
@ -117,6 +168,22 @@ export class OrganizationUsersService implements IOrganizationUsersService {
await this.organizationUsersUtilService.updateUserStatus(userId, updatedStatus, manager);
await this.licenseUserService.validateUser(manager);
await this.licenseOrganizationService.validateOrganization(manager);
const organizationIds = unarchivedUserWorkspaces.map((user) => user.organizationId);
const auditLogEntry = {
userId: user.id,
organizationIds: organizationIds,
resourceId: user.id,
resourceName: unarchivedUserWorkspaces[0].user.email,
resourceData: {
unarchived_user: {
id: unarchivedUserWorkspaces[0].userId,
email: unarchivedUserWorkspaces[0].user.email,
first_name: unarchivedUserWorkspaces[0].user.firstName,
last_name: unarchivedUserWorkspaces[0].user.lastName,
},
},
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
});
}
@ -144,6 +211,29 @@ export class OrganizationUsersService implements IOrganizationUsersService {
await this.licenseUserService.validateUser(manager);
await this.licenseOrganizationService.validateOrganization(manager);
const organization = await manager.findOne(Organization, {
where: { id: organizationUser.organizationId },
});
const auditLogEntry = {
userId: user.id,
organizationId: user.defaultOrganizationId,
resourceId: user.id,
resourceName: organizationUser.user.email,
resourceData: {
unarchived_user: {
id: organizationUser.userId,
email: organizationUser.user.email,
first_name: organizationUser.user.firstName,
last_name: organizationUser.user.lastName,
},
unarchived_user_workspace: {
workspace_name: organization.name,
workspace_id: organization.id,
},
},
};
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
});
if (organizationUser.user.invitationToken) {
@ -160,6 +250,7 @@ export class OrganizationUsersService implements IOrganizationUsersService {
sender: user.firstName,
},
});
return;
}

View file

@ -1,7 +1,7 @@
import { User } from '@entities/user.entity';
import { dbTransactionWrap } from '@helpers/database.helper';
import { fullName, generateNextNameAndSlug } from '@helpers/utils.helper';
import { EntityManager } from 'typeorm';
import { EntityManager, In } from 'typeorm';
import {
getUserStatusAndSource,
lifecycleEvents,
@ -31,8 +31,6 @@ import { UserDetailsService } from './services/user-details.service';
import { FetchUserResponse, InvitedUserType, RoleUpdate, UserFilterOptions } from './types';
import { GroupPermissionsRepository } from '@modules/group-permissions/repository';
import { ERROR_HANDLER, ERROR_HANDLER_TITLE } from '@modules/organizations/constants';
import { MODULE_INFO } from '@modules/app/constants/module-info';
import { MODULES } from '@modules/app/constants/modules';
import { INSTANCE_USER_SETTINGS } from '@modules/instance-settings/constants';
import { OrganizationRepository } from '@modules/organizations/repository';
import * as uuid from 'uuid';
@ -512,11 +510,33 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
!user || !!user.invitationToken
);
const groupsArray = [];
if (inviteNewUserDto.groups && inviteNewUserDto.groups.length > 0) {
const groupQuery = {
organizationId: currentOrganization.id,
id: In(inviteNewUserDto.groups),
};
const orgGroupPermissions = await this.groupPermissionsRepository.find({
where: groupQuery,
select: ['id', 'name'],
});
groupsArray.push(...orgGroupPermissions.map((group) => group.name));
}
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: currentUser.id,
organizationId: currentOrganization.id,
resourceId: currentOrganization.id,
resourceId: updatedUser.id,
resourceName: updatedUser.email,
resourceData: {
invited_user: {
id: updatedUser.id,
email: updatedUser.email,
first_name: updatedUser.firstName,
last_name: updatedUser.lastName,
role: inviteNewUserDto.role,
group: groupsArray,
},
},
});
return organizationUser;

Some files were not shown because too many files have changed in this diff Show more