Browse Source

Merge branch 'develop' into feat/duration-clean

pull/2389/head
Wing-Kam Wong 2 years ago
parent
commit
85d45643d5
  1. 24
      .github/workflows/ci-cd.yml
  2. 167
      .github/workflows/release-executables.yml
  3. 9
      .github/workflows/release-nightly-dev.yml
  4. 47
      .github/workflows/release-pr.yml
  5. 1
      package.json
  6. 7
      packages/nc-gui/components/CreateOrEditProject.vue
  7. 24
      packages/nc-gui/components/import/QuickImport.vue
  8. 20
      packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js
  9. 6
      packages/nc-gui/components/import/templateParsers/ExcelUrlTemplateAdapter.js
  10. 5
      packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue
  11. 28
      packages/nc-gui/components/project/spreadsheet/views/FormView.vue
  12. 7
      packages/nc-gui/pages/project/id.vue
  13. 10
      packages/nc-gui/pages/project/xcdb.vue
  14. 5
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  15. 3
      packages/nocodb/README.md
  16. 14
      packages/nocodb/package-lock.json
  17. 3
      packages/nocodb/package.json
  18. 36
      packages/nocodb/src/lib/Noco.ts
  19. 12
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts
  20. 4
      packages/nocodb/src/lib/meta/api/projectApis.ts
  21. 4
      packages/nocodb/src/lib/meta/api/projectUserApis.ts
  22. 5
      packages/nocodb/src/lib/meta/api/userApi/helpers.ts
  23. 25
      packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
  24. 6
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  25. 22
      packages/nocodb/src/lib/meta/api/utilApis.ts
  26. 5
      packages/nocodb/src/lib/meta/helpers/stringHelpers.ts
  27. 29
      packages/nocodb/src/lib/models/View.ts
  28. 23
      packages/nocodb/tests/export-import/ReadMe.md
  29. 6
      packages/nocodb/tests/export-import/config.json
  30. 297
      packages/nocodb/tests/export-import/exportSchema.js
  31. 537
      packages/nocodb/tests/export-import/importSchema.js
  32. 4759
      packages/nocodb/tests/pg-cy-quick/01-cy-quick.sql
  33. 5
      scripts/cypress/cypress.json
  34. 17
      scripts/cypress/docker-compose-pg-cy-quick.yml
  35. BIN
      scripts/cypress/fixtures/quickTest/noco_0_91_7.db
  36. 358
      scripts/cypress/integration/common/9a_QuickTest.js
  37. 36
      scripts/cypress/integration/test/quickTest.js
  38. 14
      scripts/cypress/plugins/index.js

24
.github/workflows/ci-cd.yml

@ -61,7 +61,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-pg-restTableOps-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-pg-restViews-run-cache:
@ -106,7 +106,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-pg-restViews-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-pg-restRoles-run-cache:
@ -151,7 +151,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-pg-restRoles-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-pg-restMisc-run-cache:
@ -196,7 +196,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-pg-restMisc-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restTableOps-run-cache:
@ -241,7 +241,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-restTableOps-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restViews-run-cache:
@ -286,7 +286,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-restViews-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restRoles-run-cache:
@ -331,7 +331,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-restRoles-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-restMisc-run-cache:
@ -376,7 +376,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-restMisc-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restTableOps-run-cache:
@ -421,7 +421,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-xcdb-restTableOps-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restViews-run-cache:
@ -466,7 +466,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-xcdb-restViews-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restRoles-run-cache:
@ -511,7 +511,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-xcdb-restRoles-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
cypress-xcdb-restMisc-run-cache:
@ -556,7 +556,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v2
with:
name: restTableOps-snapshots
name: cypress-xcdb-restMisc-run-cache-snapshots
path: scripts/cypress/screenshots
retention-days: 2
# docker:

167
.github/workflows/release-executables.yml

@ -0,0 +1,167 @@
name: "Release : Executables"
on:
# Triggered manually
workflow_dispatch:
inputs:
tag:
description: "Timely version"
required: true
# Triggered by release-nightly-dev.yml / release-pr.yml
workflow_call:
inputs:
tag:
description: "Timely version"
required: true
type: string
secrets:
NC_GITHUB_TOKEN:
required: true
jobs:
build-executables:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.NC_GITHUB_TOKEN }}
repository: 'nocodb/nocodb-timely'
- name: Cache node modules
id: cache-npm
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache pkg modules
id: cache-pkg
uses: actions/cache@v3
env:
cache-name: cache-pkg
with:
# pkg cache files are stored in `~/.pkg-cache`
path: ~/.pkg-cache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install QEMU and ldid
run: |
# Install qemu
sudo apt install qemu binfmt-support qemu-user-static
# install ldid
git clone https://github.com/daeken/ldid.git
cd ./ldid
./make.sh
sudo cp ./ldid /usr/local/bin
- name: Update nocodb-timely
env:
TAG: ${{ github.event.inputs.tag || inputs.tag }}
run: |
npm i -E nocodb-daily@$TAG
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git commit package.json -m "Update to $TAG"
git tag $TAG
git push --tags
- uses: actions/setup-node@v3
with:
node-version: 16
- name : Install dependencies and build executables
run: |
# install npm dependendencies
npm i
# Copy sqlite binaries
rsync -rvzhP ./binaries/binding/ ./node_modules/sqlite3/lib/binding/
# clean up code to optimize size
npx modclean --patterns="default:*" --ignore="nc-lib-gui-daily/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
# build executables
npm run build
mkdir ./mac-dist
mv ./dist/Noco-macos-arm64 ./mac-dist/
mv ./dist/Noco-macos-x64 ./mac-dist/
# Compress executables
GZIP=-9 tar -czvf ./dist/Noco-linux-x64.tar.gz ./dist/Noco-linux-x64
GZIP=-9 tar -czvf ./dist/Noco-win-x64.tar.gz ./dist/Noco-win-x64.exe
GZIP=-9 tar -czvf ./dist/Noco-linux-arm64.tar.gz ./dist/Noco-linux-arm64
GZIP=-9 tar -czvf ./dist/Noco-win-arm64.tar.gz ./dist/Noco-win-arm64.exe
- name: Upload executables(except mac executables) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN }}
file: dist/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nocodb-timely
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
retention-days: 1
sign-mac-executables:
runs-on: macos-latest
needs: build-executables
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
- name: Sign macOS executables
run: |
/usr/bin/codesign --force -s - ./mac-dist/Noco-macos-arm64 -v
/usr/bin/codesign --force -s - ./mac-dist/Noco-macos-x64 -v
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
retention-days: 1
publish-mac-executables:
needs: sign-mac-executables
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
- name: Compress files
run: |
GZIP=-9 tar -czvf ./mac-dist/Noco-macos-x64.tar.gz ./mac-dist/Noco-macos-x64
GZIP=-9 tar -czvf ./mac-dist/Noco-macos-arm64.tar.gz ./mac-dist/Noco-macos-arm64
- name: Upload mac executables to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN }}
file: mac-dist/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nocodb-timely

9
.github/workflows/release-nightly-dev.yml

@ -48,6 +48,15 @@ jobs:
secrets:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Build docker image and push to docker hub
release-docker:
needs: [set-tag, release-npm]

47
.github/workflows/release-pr.yml

@ -77,6 +77,15 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# leave-comment:
# runs-on: 'ubuntu-latest'
# needs: [release-docker, set-tag]
@ -97,6 +106,44 @@ jobs:
docker run -d -p 8888:8080 nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
```
leave-executable-comment:
if: ${{ github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false }}
runs-on: 'ubuntu-latest'
needs: [release-executables, set-tag]
steps:
- uses: peter-evans/commit-comment@v2
with:
body: |
### Run Executables
#### MacOS
```bash
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
&& cd ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
&& curl http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-macos-arm64 -o noco -L \
&& chmod +x noco \
&& ./noco
```
#### Linux
```bash
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
&& cd ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
&& curl http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-linux-x64 -o noco -L \
&& chmod +x noco \
&& ./noco
```
#### windows
```bash
iwp http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-win-arm64.exe
.\Noco-win-arm64.exe
```
For executables visit [here](https://github.com/nocodb/nocodb-timely/releases/tag/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }})
# if-merged:
# if: github.event.pull_request.merged == true
# runs-on: ubuntu-latest

1
package.json

@ -18,6 +18,7 @@
"start:api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:xcdb-api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "cd ./packages/nc-gui; npm install; npm run dev",
"cypress:run": "cypress run --config-file ./scripts/cypress/cypress.json",

7
packages/nc-gui/components/CreateOrEditProject.vue

@ -89,7 +89,7 @@
ref="name"
v-model="project.title"
v-ge="['project', 'name']"
:rules="form.titleRequiredRule"
:rules="form.titleValidationRule"
:height="20"
:label="$t('placeholder.projName')"
autofocus
@ -963,7 +963,10 @@ export default {
/** ************** START : form related ****************/
form: {
portValidationRule: [v => /^\d+$/.test(v) || 'Not a valid port'],
titleRequiredRule: [v => !!v || 'Title is required'],
titleValidationRule: [
v => !!v || 'Title is required',
v => v.length <= 50 || 'Project name exceeds 50 characters',
],
requiredRule: [v => !!v || 'Field is required'],
folderRequiredRule: [v => !!v || 'Folder path is required']
},

24
packages/nc-gui/components/import/QuickImport.vue

@ -60,11 +60,18 @@
v-model="url"
hide-details="auto"
type="url"
:label="quickImportType == 'excel' ? $t('msg.info.excelURL') : $t('msg.info.csvURL') "
:label="quickImportType === 'excel' ? $t('msg.info.excelURL') : $t('msg.info.csvURL') "
class="caption"
outlined
dense
:rules="[v => !!v || $t('general.required') ]"
:rules="
[
v => !!v || $t('general.required'),
v => !(/(10)(\.([2]([0-5][0-5]|[01234][6-9])|[1][0-9][0-9]|[1-9][0-9]|[0-9])){3}|(172)\.(1[6-9]|2[0-9]|3[0-1])(\.(2[0-4][0-9]|25[0-5]|[1][0-9][0-9]|[1-9][0-9]|[0-9])){2}|(192)\.(168)(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){2}|(0.0.0.0)|localhost?/g).test(v) || errorMessages.ipBlockList,
v => quickImportType === 'excel' ?
(/.*\.(xls|xlsx|xlsm|ods|ots)/.test(v) || errorMessages.importExcel) :
(/.*\.(csv)/.test(v) || errorMessages.importCSV),
]"
/>
<v-btn v-t="['c:project:create:excel:load-url']" class="ml-3" color="primary" @click="loadUrl">
<!--Load-->
@ -213,7 +220,12 @@ export default {
parserConfig: {
maxRowsToParse: 500
},
filename: ''
filename: '',
errorMessages: {
importExcel: "Target file is not an accepted file type. The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots!",
importCSV: "Target file is not an accepted file type. The accepted file type is .csv!",
ipBlockList: "IP Not allowed!"
}
}
},
computed: {
@ -288,7 +300,7 @@ export default {
templateGenerator = new ExcelTemplateAdapter(name, val, this.parserConfig)
break
case 'url':
templateGenerator = new ExcelUrlTemplateAdapter(val, this.$store, this.parserConfig, this.$api)
templateGenerator = new ExcelUrlTemplateAdapter(val, this.$store, this.parserConfig, this.$api, this.quickImportType)
break
}
await templateGenerator.init()
@ -322,11 +334,11 @@ export default {
if (this.quickImportType === 'excel') {
if (!/.*\.(xls|xlsx|xlsm|ods|ots)/.test(file.name)) {
return this.$toast.error('Dropped file is not an accepted file type. The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots!').goAway(3000)
return this.$toast.error(this.errorMessages.importExcel).goAway(3000)
}
} else if (this.quickImportType === 'csv') {
if (!/.*\.(csv)/.test(file.name)) {
return this.$toast.error('Dropped file is not an accepted file type. The accepted file type is .csv!').goAway(3000)
return this.$toast.error(this.errorMessages.importCSV).goAway(3000)
}
}
this._file(file)

20
packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js

@ -27,7 +27,25 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
}
async init() {
this.wb = XLSX.read(new Uint8Array(this.excelData), { type: 'array', cellText: true, cellDates: true })
const options = {
cellText: true,
cellDates: true
}
if (this.name.slice(-3) === 'csv') {
this.wb = XLSX.read(
(new TextDecoder).decode(new Uint8Array(this.excelData)),
{
type: "string",
...options
});
} else {
this.wb = XLSX.read(
new Uint8Array(this.excelData),
{
type: 'array',
...options
})
}
}
parse() {

6
packages/nc-gui/components/import/templateParsers/ExcelUrlTemplateAdapter.js

@ -1,19 +1,19 @@
import ExcelTemplateAdapter from '~/components/import/templateParsers/ExcelTemplateAdapter'
export default class ExcelUrlTemplateAdapter extends ExcelTemplateAdapter {
constructor(url, $store, parserConfig, $api) {
constructor(url, $store, parserConfig, $api, quickImportType) {
const name = url.split('/').pop()
super(name, null, parserConfig)
this.url = url
this.$api = $api
this.$store = $store
this.quickImportType = quickImportType
}
async init() {
const data = await this.$api.utils.axiosRequestMake({
apiMeta: {
url: this.url,
responseType: 'arraybuffer'
url: this.url
}
})
this.excelData = data.data

5
packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue

@ -267,10 +267,7 @@ export default {
// validate data type
if (parsedTree.callee.type === jsep.IDENTIFIER) {
const expectedType = formulas[parsedTree.callee.name].type
if (
expectedType === formulaTypes.NUMERIC ||
expectedType === formulaTypes.STRING
) {
if (expectedType === formulaTypes.NUMERIC) {
parsedTree.arguments.map(arg => this.validateAgainstType(arg, expectedType, null, typeErrors))
} else if (expectedType === formulaTypes.DATE) {
if (parsedTree.callee.name === 'DATEADD') {

28
packages/nc-gui/components/project/spreadsheet/views/FormView.vue

@ -617,35 +617,21 @@ export default {
) {
continue
}
if (
!column.virtual &&
(((column.rqd || column.notnull) && !column.cdf) ||
(column.pk && !(column.ai || column.default)) ||
this.localParams.fields[column.title].required)
) {
if (!isVirtualCol(column) && (((column.rqd || column.notnull) && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required)) {
obj.localState[column.title] = { required }
} else if (column.bt) {
const col = this.meta.columns.find(
c => c.column_name === column.bt.column_name
)
if (
(col.rqd && !col.default) ||
this.localParams.fields[column.title].required
) {
obj.localState[col.title] = { required }
} else if (column.uidt === UITypes.LinkToAnotherRecord && column.colOptions && column.colOptions.type === RelationTypes.BELONGS_TO) {
const col = this.meta.columns.find(c => c.id === column.colOptions.fk_child_column_id)
if ((col && col.rqd && !col.cdf) || column.required) {
if (col) { obj.virtual[column.title] = { required } }
}
} else if (
column.virtual &&
this.localParams.fields[column.title].required &&
(column.mm || column.hm)
) {
} else if (isVirtualCol(column) && column.required) {
obj.virtual[column.title] = {
minLength: minLength(1),
required
}
}
}
return obj
},
computed: {

7
packages/nc-gui/pages/project/id.vue

@ -84,7 +84,7 @@
<v-text-field
v-model="project.title"
v-ge="['project','name']"
:rules="form.titleRequiredRule"
:rules="form.titleValidationRule"
:height="20"
label="Enter Project Name"
autofocus
@ -650,7 +650,10 @@ export default {
/** ************** START : form related ****************/
form: {
portValidationRule: [v => /^\d+$/.test(v) || 'Not a valid port'],
titleRequiredRule: [v => !!v || 'Title is required'],
titleValidationRule: [
v => !!v || 'Title is required',
v => v.length <= 50 || 'Project name exceeds 50 characters',
],
requiredRule: [v => !!v || 'Field is required'],
folderRequiredRule: [v => !!v || 'Folder path is required']
},

10
packages/nc-gui/pages/project/xcdb.vue

@ -39,7 +39,7 @@
:full-width="false"
class="nc-metadb-project-name"
:label="$t('placeholder.projName')"
:rules="[v => !!v || $t('general.required')]"
:rules="form.titleValidationRule"
/>
<!-- <div class="wrapper mb-5 mt-5">
@ -109,7 +109,13 @@ export default {
icon: require('@/assets/img/grpc-icon-color.png'),
type: 'img'
}, */
]
],
form: {
titleValidationRule: [
v => !!v || 'Title is required',
v => v.length <= 50 || 'Project name exceeds 50 characters',
],
},
}),
computed: {
typeIcon() {

5
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -123,6 +123,7 @@ export class MssqlUi {
pk: false,
un: false,
ai: false,
au: true,
cdf: 'GETDATE()',
clen: 45,
np: null,
@ -919,7 +920,9 @@ export class MssqlUi {
// if (1) {
col.altered = col.altered || 2;
// }
if (col.au) {
col.cdf = 'GETDATE()';
}
// if (!col.ai) {
// col.dtx = 'specificType'
// } else {

3
packages/nocodb/README.md

@ -1,10 +1,11 @@
# nocodb
# Nocodb
## Running locally
Even though this package is a backend project, you can still visit the dashboard as it includes ``nc-lib-gui``.
```
npm install
npm run watch:run
# open localhost:8080/dashboard in browser

14
packages/nocodb/package-lock.json generated

@ -70,7 +70,7 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.61",
"nc-help": "0.2.65",
"nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
@ -16352,9 +16352,9 @@
}
},
"node_modules/nc-help": {
"version": "0.2.61",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.61.tgz",
"integrity": "sha512-YvFD0EjhwLs36U4GqIg0uxYWBurc39Wt7Hi7pd4/Lh1A/hmpWl6eZD4k9gk2FqqtGMlR/7e5UOmFYrRuvtzkTw==",
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.65.tgz",
"integrity": "sha512-Sia+ZhfrCCFu70khjVCUMJ7B8xYlSyfatKt7WERDzqiykC7rOGFxfyhKPfhDzep+X1CDT3R+psd4lTi4d52wdg==",
"dependencies": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",
@ -38040,9 +38040,9 @@
"integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw=="
},
"nc-help": {
"version": "0.2.61",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.61.tgz",
"integrity": "sha512-YvFD0EjhwLs36U4GqIg0uxYWBurc39Wt7Hi7pd4/Lh1A/hmpWl6eZD4k9gk2FqqtGMlR/7e5UOmFYrRuvtzkTw==",
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.65.tgz",
"integrity": "sha512-Sia+ZhfrCCFu70khjVCUMJ7B8xYlSyfatKt7WERDzqiykC7rOGFxfyhKPfhDzep+X1CDT3R+psd4lTi4d52wdg==",
"requires": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",

3
packages/nocodb/package.json

@ -69,6 +69,7 @@
"watch:serve": "nodemon -e ts -w ./build -x npm run debug-local ",
"watch:run": "cross-env NC_DISABLE_TELE1=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:cypress": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:cypress:pg": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"watch:run:mysql": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunMysql --log-error --project tsconfig.json\"",
"watch:run:pg": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"run": "ts-node src/run/docker",
@ -153,7 +154,7 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.61",
"nc-help": "0.2.65",
"nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",

36
packages/nocodb/src/lib/Noco.ts

@ -187,7 +187,7 @@ export default class Noco {
}
await Noco._ncMeta.metaInit();
await this.readOrGenJwtSecret();
await this.initJwt();
await initAdminFromEnv();
await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta });
@ -489,20 +489,28 @@ export default class Noco {
}
}
private async readOrGenJwtSecret(): Promise<any> {
if (this.config?.auth?.jwt && !this.config.auth.jwt.secret) {
let secret = (
await Noco._ncMeta.metaGet('', '', 'nc_store', {
key: 'nc_auth_jwt_secret'
})
)?.value;
if (!secret) {
await Noco._ncMeta.metaInsert('', '', 'nc_store', {
key: 'nc_auth_jwt_secret',
value: secret = uuidv4()
});
private async initJwt(): Promise<any> {
if (this.config?.auth?.jwt) {
if (!this.config.auth.jwt.secret) {
let secret = (
await Noco._ncMeta.metaGet('', '', 'nc_store', {
key: 'nc_auth_jwt_secret'
})
)?.value;
if (!secret) {
await Noco._ncMeta.metaInsert('', '', 'nc_store', {
key: 'nc_auth_jwt_secret',
value: secret = uuidv4()
});
}
this.config.auth.jwt.secret = secret;
}
this.config.auth.jwt.options = this.config.auth.jwt.options || {};
if (!this.config.auth.jwt.options?.expiresIn) {
this.config.auth.jwt.options.expiresIn =
process.env.NC_JWT_EXPIRES_IN ?? '10h';
}
this.config.auth.jwt.secret = secret;
}
let serverId = (
await Noco._ncMeta.metaGet('', '', 'nc_store', {

12
packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts

@ -5,18 +5,22 @@ import {
getViewAndModelFromRequestByAliasOrId
} from './helpers';
import apiMetrics from '../../helpers/apiMetrics';
import View from '../../../models/View';
async function csvDataExport(req: Request, res: Response) {
const { view } = await getViewAndModelFromRequestByAliasOrId(req);
const { offset, elapsed, data } = await extractCsvData(view, req);
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
let targetView = view;
if (!targetView) {
targetView = await View.getDefaultView(model.id);
}
const { offset, elapsed, data } = await extractCsvData(targetView, req);
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,
'nc-export-elapsed-time': elapsed,
'Content-Disposition': `attachment; filename="${encodeURI(
view.title
targetView.title
)}-export.csv"`
});
res.send(data);

4
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -95,6 +95,10 @@ async function projectCreate(req: Request<any, any>, res) {
projectBody.is_meta = false;
}
if (projectBody?.title.length > 50) {
NcError.badRequest('Project title exceeds 50 characters');
}
if (await Project.getByTitle(projectBody?.title)) {
NcError.badRequest('Project title already in use');
}

4
packages/nocodb/src/lib/meta/api/projectUserApis.ts

@ -15,6 +15,7 @@ import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
import Noco from '../../Noco';
import { PluginCategory } from 'nocodb-sdk';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { randomTokenString } from '../helpers/stringHelpers';
async function userList(req, res) {
res.json({
@ -101,7 +102,8 @@ async function userInvite(req, res, next): Promise<any> {
invite_token,
invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
email,
roles: 'user'
roles: 'user',
token_version: randomTokenString()
});
// add user to project

5
packages/nocodb/src/lib/meta/api/userApi/helpers.ts

@ -1,4 +1,5 @@
import * as jwt from 'jsonwebtoken';
import crypto from 'crypto';
import User from '../../../models/User';
import { NcConfig } from '../../../../interface/config';
@ -16,3 +17,7 @@ export function genJwt(user: User, config: NcConfig) {
config.auth.jwt.options
);
}
export function randomTokenString(): string {
return crypto.randomBytes(40).toString('hex');
}

25
packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts

@ -2,17 +2,17 @@ import User from '../../../models/User';
import ProjectUser from '../../../models/ProjectUser';
import { promisify } from 'util';
import { Strategy as CustomStrategy } from 'passport-custom';
import { Strategy } from 'passport-jwt';
import passport from 'passport';
import { ExtractJwt } from 'passport-jwt';
import passportJWT from 'passport-jwt';
import { Strategy as AuthTokenStrategy } from 'passport-auth-token';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { randomTokenString } from '../../helpers/stringHelpers';
const PassportLocalStrategy = require('passport-local').Strategy;
const ExtractJwt = passportJWT.ExtractJwt;
const JwtStrategy = passportJWT.Strategy;
const jwtOptions = {
expiresIn: process.env.NC_JWT_EXPIRES_IN ?? '10h',
jwtFromRequest: ExtractJwt.fromHeader('xc-auth')
};
@ -83,7 +83,7 @@ export function initStrategies(router): void {
});
passport.use(
new Strategy(
new JwtStrategy(
{
secretOrKey: Noco.getConfig().auth.jwt.secret,
...jwtOptions,
@ -102,7 +102,11 @@ export function initStrategies(router): void {
);
if (cachedVal) {
if (cachedVal.token_version !== jwtPayload.token_version) {
if (
cachedVal.token_version &&
jwtPayload.token_version &&
cachedVal.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));
}
return done(null, cachedVal);
@ -110,7 +114,11 @@ export function initStrategies(router): void {
User.getByEmail(jwtPayload?.email)
.then(async user => {
if (user.token_version !== jwtPayload.token_version) {
if (
user.token_version &&
jwtPayload.token_version &&
user.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));
}
if (req.ncProjectId) {
@ -266,7 +274,8 @@ export function initStrategies(router): void {
password: '',
salt,
roles,
email_verified: true
email_verified: true,
token_version: randomTokenString()
});
return done(null, user);
}

6
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -11,7 +11,6 @@ import { Tele } from 'nc-help';
const { v4: uuidv4 } = require('uuid');
import Audit from '../../../models/Audit';
import crypto from 'crypto';
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2';
import passport from 'passport';
@ -20,6 +19,7 @@ import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import { MetaTable } from '../../../utils/globals';
import Noco from '../../../Noco';
import { genJwt } from './helpers';
import { randomTokenString } from '../../helpers/stringHelpers';
export async function signup(req: Request, res: Response<TableType>) {
const {
@ -243,10 +243,6 @@ async function googleSignin(req, res, next) {
)(req, res, next);
}
function randomTokenString(): string {
return crypto.randomBytes(40).toString('hex');
}
function setTokenCookie(res, token): void {
// create http only cookie with refresh token that expires in 7 days
const cookieOptions = {

22
packages/nocodb/src/lib/meta/api/utilApis.ts

@ -60,8 +60,9 @@ export async function releaseVersion(_req: Request, res: Response) {
res.json(result);
}
export async function axiosRequestMake(req: Request, res: Response) {
async function _axiosRequestMake(req: Request, res: Response) {
const { apiMeta } = req.body;
if (apiMeta?.body) {
try {
apiMeta.body = JSON.parse(apiMeta.body);
@ -106,6 +107,25 @@ export async function axiosRequestMake(req: Request, res: Response) {
return res.json(data?.data);
}
export async function axiosRequestMake(req: Request, res: Response) {
const {
apiMeta: { url }
} = req.body;
const isExcelImport = /.*\.(xls|xlsx|xlsm|ods|ots)/;
const isCSVImport = /.*\.(csv)/;
const ipBlockList = /(10)(\.([2]([0-5][0-5]|[01234][6-9])|[1][0-9][0-9]|[1-9][0-9]|[0-9])){3}|(172)\.(1[6-9]|2[0-9]|3[0-1])(\.(2[0-4][0-9]|25[0-5]|[1][0-9][0-9]|[1-9][0-9]|[0-9])){2}|(192)\.(168)(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){2}|(0.0.0.0)|localhost?/g;
if (
ipBlockList.test(url) ||
(!isCSVImport.test(url) && !isExcelImport.test(url))
) {
return res.json({});
}
if (isCSVImport || isExcelImport) {
req.body.apiMeta.responseType = 'arraybuffer';
}
return await _axiosRequestMake(req, res);
}
export default router => {
router.post(
'/api/v1/db/meta/connection/test',

5
packages/nocodb/src/lib/meta/helpers/stringHelpers.ts

@ -0,0 +1,5 @@
import crypto from 'crypto';
export function randomTokenString(): string {
return crypto.randomBytes(40).toString('hex');
}

29
packages/nocodb/src/lib/models/View.ts

@ -157,6 +157,32 @@ export default class View implements ViewType {
return viewId && this.get(viewId?.id || viewId);
}
public static async getDefaultView(
fk_model_id: string,
ncMeta = Noco.ncMeta
) {
let view =
fk_model_id &&
(await NocoCache.get(
`${CacheScope.VIEW}:${fk_model_id}:default`,
CacheGetType.TYPE_OBJECT
));
if (!view) {
view = await ncMeta.metaGet2(
null,
null,
MetaTable.VIEWS,
{
fk_model_id,
is_default: 1
},
null
);
await NocoCache.set(`${CacheScope.VIEW}:${fk_model_id}:default`, view);
}
return view && new View(view);
}
public static async list(modelId: string, ncMeta = Noco.ncMeta) {
let viewsList = await NocoCache.getList(CacheScope.VIEW, [modelId]);
if (!viewsList.length) {
@ -666,6 +692,9 @@ export default class View implements ViewType {
if (o) {
// update data
o = { ...o, ...updateObj };
if (o.is_default) {
await NocoCache.set(`${CacheScope.VIEW}:${o.fk_model_id}:default`, o);
}
// set cache
await NocoCache.set(key, o);
}

23
packages/nocodb/tests/export-import/ReadMe.md

@ -0,0 +1,23 @@
## config.json
{
"srcProject": "sample",
"dstProject": "sample-copy",
"baseURL": "http://localhost:8080",
"xc-auth": "Copy Auth Token"
}
- baseURL & xc-auth are common configurations for both import & export
## Export
- `srcProject`: specify source project name to be exported.
- Export JSON file will be created as `srcProject.json`
- execute
`cd packages/nocodb/tests/export-import`
`node exportSchema.js`
## Import
- `srcProject`: specify JSON file name to be imported (sans .JSON suffix)
- `dstProject`: new project name to be imported as
- Data will also be imported if `srcProject` exists in NocoDB. Note that, data import isn't via exported JSON
- execute
`cd packages/nocodb/tests/export-import`
`node importSchema.js`

6
packages/nocodb/tests/export-import/config.json

@ -0,0 +1,6 @@
{
"srcProject": "sample",
"dstProject": "sample-copy",
"baseURL": "http://localhost:8080",
"xc-auth": "Copy Auth Token"
}

297
packages/nocodb/tests/export-import/exportSchema.js

@ -0,0 +1,297 @@
const Api = require('nocodb-sdk').Api;
const { UITypes } = require('nocodb-sdk');
const jsonfile = require('jsonfile');
const GRID = 3, GALLERY = 2, FORM = 1;
let ncMap = { /* id: name <string> */ };
let tblSchema = [];
let api = {};
let viewStore = { columns: {}, sort: {}, filter: {} };
let inputConfig = jsonfile.readFileSync(`config.json`)
let ncConfig = {
projectName: inputConfig.srcProject,
baseURL: inputConfig.baseURL,
headers: {
'xc-auth': `${inputConfig["xc-auth"]}`
}
};
// helper routines
// remove objects containing 0/ false/ null
// fixme: how to handle when cdf (default value) is configured as 0/ null/ false
function removeEmpty(obj) {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null && v != 0 && v != false)
.map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : v])
);
}
function addColumnSpecificData(c) {
// pick required fields to proceed further
let col = removeEmpty(
(({ id, title, column_name, uidt, dt, pk, pv, rqd, dtxp, system }) => ({
id,
title,
column_name,
uidt,
dt,
pk,
pv,
rqd,
dtxp,
system
}))(c)
);
switch (c.uidt) {
case UITypes.Formula:
col.formula = c.colOptions.formula;
col.formula_raw = c.colOptions.formula_raw;
break;
case UITypes.LinkToAnotherRecord:
col[`colOptions`] = {
fk_model_id: c.fk_model_id,
fk_related_model_id: c.colOptions.fk_related_model_id,
fk_child_column_id: c.colOptions.fk_child_column_id,
fk_parent_column_id: c.colOptions.fk_parent_column_id,
type: c.colOptions.type
};
break;
case UITypes.Lookup:
col[`colOptions`] = {
fk_model_id: c.fk_model_id,
fk_relation_column_id: c.colOptions.fk_relation_column_id,
fk_lookup_column_id: c.colOptions.fk_lookup_column_id
};
break;
case UITypes.Rollup:
col[`colOptions`] = {
fk_model_id: c.fk_model_id,
fk_relation_column_id: c.colOptions.fk_relation_column_id,
fk_rollup_column_id: c.colOptions.fk_rollup_column_id,
rollup_function: c.colOptions.rollup_function
};
break;
}
return col;
}
function addViewDetails(v) {
// pick required fields to proceed further
let view = (({ id, title, type, show_system_fields, lock_type, order }) => ({
id,
title,
type,
show_system_fields,
lock_type,
order
}))(v);
// form view
if (v.type === FORM) {
view.property = (({
heading,
subheading,
success_msg,
redirect_after_secs,
email,
submit_another_form,
show_blank_form
}) => ({
heading,
subheading,
success_msg,
redirect_after_secs,
email,
submit_another_form,
show_blank_form
}))(v.view);
}
// gallery view
else if (v.type === GALLERY) {
view.property = {
fk_cover_image_col_id: ncMap[v.view.fk_cover_image_col_id]
};
}
// gallery view doesn't share column information in api yet
if (v.type !== GALLERY) {
if (v.type === GRID)
view.columns = viewStore.columns[v.id].map(a =>
(({ id, width, order, show }) => ({ id, width, order, show }))(a)
);
if (v.type === FORM)
view.columns = viewStore.columns[v.id].map(a =>
(({ id, order, show, label, help, description, required }) => ({
id,
order,
show,
label,
help,
description,
required
}))(a)
);
for (let i = 0; i < view.columns?.length; i++)
view.columns[i].title = ncMap[viewStore.columns[v.id][i].id];
// skip hm & mm columns
view.columns = view.columns
?.filter(a => a.title?.includes('_nc_m2m_') === false)
.filter(a => a.title?.includes('nc_') === false);
}
// filter & sort configurations
if (v.type !== FORM) {
view.sort = viewStore.sort[v.id].map(a =>
(({ fk_column_id, direction, order }) => ({
fk_column_id,
direction,
order
}))(a)
);
view.filter = viewStore.filter[v.id].map(a =>
(({ fk_column_id, logical_op, comparison_op, value, order }) => ({
fk_column_id,
logical_op,
comparison_op,
value,
order
}))(a)
);
}
return view;
}
// view data stored as is for quick access
async function storeViewDetails(tableId) {
// read view data for each table
let viewList = await api.dbView.list(tableId);
for (let j = 0; j < viewList.list.length; j++) {
let v = viewList.list[j];
let viewDetails = [];
// invoke view specific read to populate columns information
if (v.type === FORM) viewDetails = (await api.dbView.formRead(v.id)).columns;
else if (v.type === GALLERY) viewDetails = await api.dbView.galleryRead(v.id);
else if (v.type === GRID) viewDetails = await api.dbView.gridColumnsList(v.id);
viewStore.columns[v.id] = viewDetails;
// populate sort information
let vSort = await api.dbTableSort.list(v.id);
viewStore.sort[v.id] = vSort.sorts.list;
let vFilter = await api.dbTableFilter.read(v.id);
viewStore.filter[v.id] = vFilter;
}
}
// mapping table for quick information access
// store maps for tableId, columnId, viewColumnId & viewId to their names
async function generateMapTbl(pId) {
const tblList = await api.dbTable.list(pId);
for (let i = 0; i < tblList.list.length; i++) {
let tblId = tblList.list[i].id;
let tbl = await api.dbTable.read(tblId);
// table ID <> name
ncMap[tblId] = tbl.title;
// column ID <> name
tbl.columns.map(x => (ncMap[x.id] = x.title));
// view ID <> name
tbl.views.map(x => (ncMap[x.id] = x.tn));
for (let i = 0; i < tbl.views.length; i++) {
let x = tbl.views[i];
let viewColumns = [];
if (x.type === FORM) viewColumns = (await api.dbView.formRead(x.id)).columns;
else if (x.type === GALLERY)
viewColumns = (await api.dbView.galleryRead(x.id)).columns;
else if (x.type === GRID) viewColumns = await api.dbView.gridColumnsList(x.id);
// view column ID <> name
viewColumns?.map(a => (ncMap[a.id] = ncMap[a.fk_column_id]));
}
}
}
// main
//
async function exportSchema() {
api = new Api(ncConfig);
// fetch project details (id et.al)
const x = await api.project.list();
const p = x.list.find(a => a.title === ncConfig.projectName);
await generateMapTbl(p.id);
// read project
const tblList = await api.dbTable.list(p.id);
// for each table
for (let i = 0; i < tblList.list.length; i++) {
let tblId = tblList.list[i].id;
await storeViewDetails(tblId);
let tbl = await api.dbTable.read(tblId);
// prepare schema
let tSchema = {
id: tbl.id,
title: tbl.title,
table_name: tbl?.table_name,
columns: [...tbl.columns.map(c => addColumnSpecificData(c))]
.filter(a => a.title.includes('_nc_m2m_') === false) // mm
.filter(a => a.title.includes(p.prefix) === false) // hm
.filter(
a => !(a?.system === 1 && a.uidt === UITypes.LinkToAnotherRecord)
),
views: [...tbl.views.map(v => addViewDetails(v))]
};
tblSchema.push(tSchema);
}
}
(async () => {
await exportSchema();
jsonfile.writeFileSync(
`${ncConfig.projectName.replace(/ /g, '_')}.json`,
tblSchema,
{ spaces: 2 }
);
})().catch(e => {
console.log(e);
});
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Raju Udava <sivadstala@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

537
packages/nocodb/tests/export-import/importSchema.js

@ -0,0 +1,537 @@
// tbd
// - formula dependency list
// - nested lookup/ rollup
const Api = require('nocodb-sdk').Api;
const { UITypes } = require('nocodb-sdk');
const jsonfile = require('jsonfile');
let inputConfig = jsonfile.readFileSync(`config.json`)
let ncConfig = {
srcProject: inputConfig.srcProject,
projectName: inputConfig.dstProject,
baseURL: inputConfig.baseURL,
headers: {
'xc-auth': `${inputConfig["xc-auth"]}`
}
};
let ncIn = jsonfile.readFileSync(`${ncConfig.srcProject}.json`);
let api = {};
let ncProject = {};
let link = [];
let lookup = [];
let rollup = [];
let formula = [];
let rootLinks = [];
// maps v1 table ID, v2 table ID & table title to table schema
let ncTables = {};
async function createBaseTables() {
console.log(`createBaseTables`);
for (let i = 0; i < ncIn.length; i++) {
let tblSchema = ncIn[i];
let reducedColumnSet = tblSchema.columns.filter(
a =>
a.uidt !== UITypes.LinkToAnotherRecord &&
a.uidt !== UITypes.Lookup &&
a.uidt !== UITypes.Rollup &&
a.uidt !== UITypes.Formula
);
link.push(
...tblSchema.columns.filter(a => a.uidt === UITypes.LinkToAnotherRecord)
);
lookup.push(...tblSchema.columns.filter(a => a.uidt === UITypes.Lookup));
rollup.push(...tblSchema.columns.filter(a => a.uidt === UITypes.Rollup));
formula.push(...tblSchema.columns.filter(a => a.uidt === UITypes.Formula));
formula.map(a => (a['table_id'] = tblSchema.id));
let tbl = await api.dbTable.create(ncProject.id, {
title: tblSchema.title,
table_name: tblSchema.title,
columns: reducedColumnSet.map(({ id, ...rest }) => ({ ...rest }))
});
ncTables[tbl.title] = tbl;
ncTables[tbl.id] = tbl;
ncTables[tblSchema.id] = tbl;
}
}
let linksCreated = [];
function isLinkCreated(pId, cId) {
let idx = linksCreated.findIndex(a => a.cId === pId && a.pId === cId);
if (idx === -1) {
linksCreated.push({ pId: pId, cId: cId });
return false;
}
return true;
}
// retrieve nc-view column ID from corresponding nc-column ID
async function nc_getViewColumnId(viewId, viewType, ncColumnId) {
// retrieve view Info
let viewDetails;
if (viewType === 'form')
viewDetails = (await api.dbView.formRead(viewId)).columns;
else if (viewType === 'gallery')
viewDetails = (await api.dbView.galleryRead(viewId)).columns;
else viewDetails = await api.dbView.gridColumnsList(viewId);
return viewDetails.find(x => x.fk_column_id === ncColumnId)?.id;
}
async function createFormula() {
for (let i = 0; i < formula.length; i++) {
let tbl = await api.dbTableColumn.create(ncTables[formula[i].table_id].id, {
uidt: UITypes.Formula,
title: formula[i].title,
formula_raw: formula[i].formula_raw
});
}
}
async function createLinks() {
console.log(`createLinks`);
for (let i = 0; i < link.length; i++) {
if (
(link[i].colOptions.type === 'mm' &&
false ===
isLinkCreated(
link[i].colOptions.fk_parent_column_id,
link[i].colOptions.fk_child_column_id
)) ||
link[i].colOptions.type === 'hm'
) {
let srcTbl = ncTables[link[i].colOptions.fk_model_id];
let dstTbl = ncTables[link[i].colOptions.fk_related_model_id];
// create link
let tbl = await api.dbTableColumn.create(srcTbl.id, {
uidt: UITypes.LinkToAnotherRecord,
title: link[i].title,
parentId: srcTbl.id,
childId: dstTbl.id,
type: link[i].colOptions.type
});
ncTables[tbl.title] = tbl;
ncTables[tbl.id] = tbl;
ncTables[link[i].colOptions.fk_model_id] = tbl;
// for data-link procedure later
rootLinks.push({ linkColumn: link[i], linkSrcTbl: srcTbl });
// symmetry field update
//
let v2ColSchema = tbl.columns.find(x => x.title === link[i].title);
// read related table again after link is created
dstTbl = await api.dbTable.read(dstTbl.id);
let v2SymmetricColumn =
link[i].colOptions.type === 'mm'
? dstTbl.columns.find(
x =>
x.uidt === UITypes.LinkToAnotherRecord &&
x?.colOptions.fk_parent_column_id ===
v2ColSchema.colOptions.fk_child_column_id &&
x?.colOptions.fk_child_column_id ===
v2ColSchema.colOptions.fk_parent_column_id
)
: dstTbl.columns.find(
x =>
x.uidt === UITypes.LinkToAnotherRecord &&
x?.colOptions.fk_parent_column_id ===
v2ColSchema.colOptions.fk_parent_column_id &&
x?.colOptions.fk_child_column_id ===
v2ColSchema.colOptions.fk_child_column_id
);
let v1SymmetricColumn =
link[i].colOptions.type === 'mm'
? link.find(
x =>
x.colOptions.fk_parent_column_id ===
link[i].colOptions.fk_child_column_id &&
x.colOptions.fk_child_column_id ===
link[i].colOptions.fk_parent_column_id &&
x.colOptions.type === 'mm'
)
: link.find(
x =>
x.colOptions.fk_parent_column_id ===
link[i].colOptions.fk_parent_column_id &&
x.colOptions.fk_child_column_id ===
link[i].colOptions.fk_child_column_id &&
x.colOptions.type === 'bt'
);
tbl = await api.dbTableColumn.update(v2SymmetricColumn.id, {
...v2SymmetricColumn,
title: v1SymmetricColumn.title,
column_name: null
});
ncTables[tbl.title] = tbl;
ncTables[tbl.id] = tbl;
ncTables[v1SymmetricColumn.colOptions.fk_model_id] = tbl;
}
}
}
function get_v2Id(v1ColId) {
for (let i = 0; i < ncIn.length; i++) {
let tblSchema = ncIn[i];
let colSchema = {};
if (
undefined !== (colSchema = tblSchema.columns.find(x => x.id === v1ColId))
) {
let colName = colSchema.title;
let v2Tbl = ncTables[tblSchema.id];
return v2Tbl.columns.find(y => y.title === colName)?.id;
}
}
}
async function createLookup() {
console.log(`createLookup`);
for (let i = 0; i < lookup.length; i++) {
let srcTbl = ncTables[lookup[i].colOptions.fk_model_id];
let v2_fk_relation_column_id = get_v2Id(
lookup[i].colOptions.fk_relation_column_id
);
let v2_lookup_column_id = get_v2Id(
lookup[i].colOptions.fk_lookup_column_id
);
if (v2_lookup_column_id) {
let tbl = await api.dbTableColumn.create(srcTbl.id, {
uidt: UITypes.Lookup,
title: lookup[i].title,
fk_relation_column_id: v2_fk_relation_column_id,
fk_lookup_column_id: v2_lookup_column_id
});
ncTables[tbl.title] = tbl;
ncTables[tbl.id] = tbl;
ncTables[lookup[i].colOptions.fk_model_id] = tbl;
}
}
}
async function createRollup() {
console.log(`createRollup`);
for (let i = 0; i < rollup.length; i++) {
let srcTbl = ncTables[rollup[i].colOptions.fk_model_id];
let v2_fk_relation_column_id = get_v2Id(
rollup[i].colOptions.fk_relation_column_id
);
let v2_rollup_column_id = get_v2Id(
rollup[i].colOptions.fk_rollup_column_id
);
if (v2_rollup_column_id) {
let tbl = await api.dbTableColumn.create(srcTbl.id, {
uidt: UITypes.Rollup,
title: rollup[i].title,
column_name: rollup[i].title,
fk_relation_column_id: v2_fk_relation_column_id,
fk_rollup_column_id: v2_rollup_column_id,
rollup_function: rollup[i].colOptions.rollup_function
});
ncTables[tbl.title] = tbl;
ncTables[tbl.id] = tbl;
ncTables[rollup[i].colOptions.fk_model_id] = tbl;
}
}
}
async function configureGrid() {
console.log(`configureGrid`);
for (let i = 0; i < ncIn.length; i++) {
let tblSchema = ncIn[i];
let tblId = ncTables[tblSchema.id].id;
let gridList = tblSchema.views.filter(a => a.type === 3);
let srcTbl = await api.dbTable.read(tblId);
const view = await api.dbView.list(tblId);
// create / rename view
for (let gridCnt = 0; gridCnt < gridList.length; gridCnt++) {
let viewCreated = {};
// rename first view; default view already created
if (gridCnt === 0) {
viewCreated = await api.dbView.update(view.list[0].id, {
title: gridList[gridCnt].title
});
}
// create new views
else {
viewCreated = await api.dbView.gridCreate(tblId, {
title: gridList[gridCnt].title
});
}
// retrieve view Info
let viewId = viewCreated.id;
let viewDetails = await api.dbView.gridColumnsList(viewId);
// column visibility
for (
let colCnt = 0;
colCnt < gridList[gridCnt].columns.length;
colCnt++
) {
let ncColumnId = srcTbl.columns.find(
a => a.title === gridList[gridCnt].columns[colCnt].title
)?.id;
// let ncViewColumnId = await nc_getViewColumnId( viewCreated.id, "grid", ncColumnId )
let ncViewColumnId = viewDetails.find(
x => x.fk_column_id === ncColumnId
)?.id;
// column order & visibility
await api.dbViewColumn.update(viewCreated.id, ncViewColumnId, {
show: gridList[gridCnt].columns[colCnt].show,
order: gridList[gridCnt].columns[colCnt].order
});
await api.dbView.gridColumnUpdate(ncViewColumnId, {
width: gridList[gridCnt].columns[colCnt].width
});
}
// sort
for (let sCnt = 0; sCnt < gridList[gridCnt].sort.length; sCnt++) {
let sColName = tblSchema.columns.find(
a => gridList[gridCnt].sort[sCnt].fk_column_id === a.id
).title;
await api.dbTableSort.create(viewId, {
fk_column_id: srcTbl.columns.find(a => a.title === sColName)?.id,
direction: gridList[gridCnt].sort[sCnt].direction
});
}
// filter
for (let fCnt = 0; fCnt < gridList[gridCnt].filter.length; fCnt++) {
let fColName = tblSchema.columns.find(
a => gridList[gridCnt].sort[fCnt].fk_column_id === a.id
).title;
await api.dbTableFilter.create(viewId, {
...gridList[gridCnt].filter[fCnt],
fk_column_id: srcTbl.columns.find(a => a.title === fColName)?.id
});
}
}
}
}
async function configureGallery() {
console.log(`configureGallery`);
for (let i = 0; i < ncIn.length; i++) {
let tblSchema = ncIn[i];
let tblId = ncTables[tblSchema.id].id;
let galleryList = tblSchema.views.filter(a => a.type === 2);
for (let cnt = 0; cnt < galleryList.length; cnt++) {
const viewCreated = await api.dbView.galleryCreate(tblId, {
title: galleryList[cnt].title
});
}
}
}
async function configureForm() {
console.log(`configureForm`);
for (let i = 0; i < ncIn.length; i++) {
let tblSchema = ncIn[i];
let tblId = ncTables[tblSchema.id].id;
let formList = tblSchema.views.filter(a => a.type === 1);
let srcTbl = await api.dbTable.read(tblId);
for (let formCnt = 0; formCnt < formList.length; formCnt++) {
const formData = {
title: formList[formCnt].title,
...formList[formCnt].property
};
const viewCreated = await api.dbView.formCreate(tblId, formData);
// column visibility
for (
let colCnt = 0;
colCnt < formList[formCnt].columns.length;
colCnt++
) {
let ncColumnId = srcTbl.columns.find(
a => a.title === formList[formCnt].columns[colCnt].title
)?.id;
let ncViewColumnId = await nc_getViewColumnId(
viewCreated.id,
'form',
ncColumnId
);
// column order & visibility
await api.dbView.formColumnUpdate(ncViewColumnId, {
show: formList[formCnt].columns[colCnt].show,
order: formList[formCnt].columns[colCnt].order,
label: formList[formCnt].columns[colCnt].label,
description: formList[formCnt].columns[colCnt].description,
required: formList[formCnt].columns[colCnt].required
});
}
}
}
}
async function restoreBaseData() {
console.log(`restoreBaseData`);
for (let i = 0; i < ncIn.length; i++) {
let tblSchema = ncIn[i];
let tblId = ncTables[tblSchema.id].id;
let pk = tblSchema.columns.find(a => a.pk).title;
let moreRecords = true;
let offset = 0,
limit = 25;
while (moreRecords) {
let recList = await api.dbTableRow.list(
'nc',
ncConfig.srcProject,
tblSchema.title,
{},
{
query: { limit: limit, offset: offset }
}
);
moreRecords = !recList.pageInfo.isLastPage;
offset += limit;
for (let recCnt = 0; recCnt < recList.list.length; recCnt++) {
let record = await api.dbTableRow.read(
'nc',
ncConfig.srcProject,
tblSchema.title,
recList.list[recCnt][pk]
);
// post-processing on the record
for (const [key, value] of Object.entries(record)) {
let table = ncTables[tblId];
// retrieve datatype
const dt = table.columns.find(x => x.title === key)?.uidt;
if (dt === UITypes.LinkToAnotherRecord) delete record[key];
if (dt === UITypes.Lookup) delete record[key];
if (dt === UITypes.Rollup) delete record[key];
}
await api.dbTableRow.create(
'nc',
ncConfig.projectName,
tblSchema.title,
record
);
}
}
}
}
async function restoreLinks() {
console.log(`restoreLinks`);
for (let i = 0; i < rootLinks.length; i++) {
let pk = rootLinks[i].linkSrcTbl.columns.find(a => a.pk).title;
let moreRecords = true;
let offset = 0,
limit = 25;
while (moreRecords) {
let recList = await api.dbTableRow.list(
'nc',
ncConfig.srcProject,
rootLinks[i].linkSrcTbl.title,
{},
{
query: { limit: limit, offset: offset }
}
);
moreRecords = !recList.pageInfo.isLastPage;
offset += limit;
for (let recCnt = 0; recCnt < recList.list.length; recCnt++) {
let record = await api.dbTableRow.read(
'nc',
ncConfig.srcProject,
rootLinks[i].linkSrcTbl.title,
recList.list[recCnt][pk]
);
let linkField = record[rootLinks[i].linkColumn.title];
if (linkField.length) {
await api.dbTableRow.nestedAdd(
'nc',
ncConfig.projectName,
rootLinks[i].linkSrcTbl.title,
record[pk],
rootLinks[i].linkColumn.colOptions.type,
encodeURIComponent(rootLinks[i].linkColumn.title),
linkField[0][pk]
);
}
}
}
}
}
async function importSchema() {
api = new Api(ncConfig);
const x = await api.project.list();
const p = x.list.find(a => a.title === ncConfig.projectName);
if (p) await api.project.delete(p.id);
ncProject = await api.project.create({ title: ncConfig.projectName });
await createBaseTables();
await createLinks();
await createLookup();
await createRollup();
await createFormula();
// configure views
await configureGrid();
await configureGallery();
await configureForm();
// restore data only if source project exists
const p2 = x.list.find(a => a.title === ncConfig.srcProject);
if (p2 !== undefined) {
await restoreBaseData();
await restoreLinks();
}
}
(async () => {
await importSchema();
console.log('completed');
})().catch(e => console.log(e));
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Raju Udava <sivadstala@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

4759
packages/nocodb/tests/pg-cy-quick/01-cy-quick.sql

File diff suppressed because it is too large Load Diff

5
scripts/cypress/cypress.json

@ -20,7 +20,8 @@
"test/pg-restTableOps.js",
"test/pg-restViews.js",
"test/pg-restRoles.js",
"test/pg-restMisc.js"
"test/pg-restMisc.js",
"common/9a_QuickTest.js"
],
"defaultCommandTimeout": 13000,
"pageLoadTimeout": 600000,
@ -47,7 +48,7 @@
"screenshot": false,
"airtable": {
"apiKey": "keyn1MR87qgyUsYg4",
"sharedBase": "https://airtable.com/shrkSQdtKNzUfAbIY"
"sharedBase": "https://airtable.com/shr4z0qmh6dg5s3eB"
}
},
"fixturesFolder": "scripts/cypress/fixtures",

17
scripts/cypress/docker-compose-pg-cy-quick.yml

@ -0,0 +1,17 @@
version: "2.1"
services:
pg96:
image: postgres:9.6
restart: always
environment:
POSTGRES_PASSWORD: password
ports:
- 5432:5432
volumes:
- ../../packages/nocodb/tests/pg-cy-quick:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5

BIN
scripts/cypress/fixtures/quickTest/noco_0_91_7.db

Binary file not shown.

358
scripts/cypress/integration/common/9a_QuickTest.js

@ -0,0 +1,358 @@
import {
isTestSuiteActive,
roles,
} from "../../support/page_objects/projectConstants";
import { loginPage, projectsPage } from "../../support/page_objects/navigation";
import { mainPage } from "../../support/page_objects/mainPage";
// normal fields
let records = {
Name: "Movie-1",
Notes: "Good",
Status: "Todo",
Tags: "Jan",
Phone: "123123123",
Email: "a@b.com",
URL: "www.a.com",
Number: "1",
Value: "$1.00",
Percent: "0.01",
Duration: "60",
};
// links/ computed fields
let records2 = {
Done: true,
Date: "2022-05-31",
Rating: "1",
Actor: ["Actor1", "Actor2"],
"Status (from Actor)": ["Todo", "In progress"],
RollUp: "128",
Computation: "4.04",
Producer: ["P1", "P2"]
};
function openWebhook(index) {
cy.get(".nc-btn-webhook").should("exist").click();
cy.get(".nc-hook").eq(index).click({ force: true });
}
// to be invoked after open
function verifyWebhook(config) {
cy.get(".nc-text-field-hook-title")
.find('input').then(($element) => {
expect($element[0].value).to.have.string(config.title)
})
cy.get(".nc-text-field-hook-event")
.find('.v-select__selection')
.contains(config.event)
.should('exist')
cy.get(".nc-text-field-hook-notification-type")
.find('.v-select__selection')
.contains(config.notification)
.should('exist')
cy.get('.nc-select-hook-url-method')
.find('.v-select__selection')
.contains(config.type)
.should('exist')
cy.get(".nc-text-field-hook-url-path")
.find('input').then(($element) => {
expect($element[0].value).to.have.string(config.url)
})
cy.get(".nc-icon-hook-navigate-left").click({force:true})
}
export const genTest = (apiType, dbType) => {
if (!isTestSuiteActive(apiType, dbType)) return;
describe(`Webhook`, () => {
before(() => {
cy.task("copyFile")
loginPage.signIn(roles.owner.credentials);
projectsPage.openProject("sample");
});
after(() => {});
it("Verify Data types", () => {
cy.openTableTab("Film", 3);
// normal cells
for (let [key, value] of Object.entries(records)) {
mainPage.getCell(key, 1).contains(value).should("exist");
}
// checkbox
mainPage
.getCell("Done", 1)
.find(".mdi-check-circle-outline")
.should(records2.Done ? "exist" : "not.exist");
// date
// rating
mainPage
.getCell("Rating", 1)
.find("button.mdi-star")
.should("have.length", records2.Rating);
// LinkToAnotherRecord
mainPage.getCell("Actor", 1).scrollIntoView();
cy.get(
':nth-child(1) > [data-col="Actor"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(1) > .v-chip__content > .name'
)
.contains(records2.Actor[0])
.should("exist");
cy.get(
':nth-child(1) > [data-col="Actor"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(2) > .v-chip__content > .name'
)
.contains(records2.Actor[1])
.should("exist");
// mainPage.getCell("Actor", 1).find(".nc-virtual-cell > .v-lazy > .d-100 > .chips").eq(0).contains("Actor1").should('exist')
// mainPage.getCell("Actor", 1).find(".nc-virtual-cell > .v-lazy > .d-100 > .chips").eq(1).contains("Actor2").should('exist')
// lookup
mainPage.getCell("Status (from Actor)", 1).scrollIntoView();
cy.get(
':nth-child(1) > [data-col="Status (from Actor)"] > .nc-virtual-cell > .v-lazy > .d-flex > :nth-child(1) > .v-chip__content > div > .set-item'
)
.contains(records2["Status (from Actor)"][0])
.should("exist");
cy.get(
':nth-child(1) > [data-col="Status (from Actor)"] > .nc-virtual-cell > .v-lazy > .d-flex > :nth-child(2) > .v-chip__content > div > .set-item'
)
.contains(records2["Status (from Actor)"][1])
.should("exist");
// rollup
mainPage.getCell("RollUp", 1).scrollIntoView();
// cy.get(':nth-child(1) > [data-col="RollUp"] > .nc-virtual-cell > .v-lazy > span').contains(records2.RollUp).should('exist')
cy.get(`:nth-child(1) > [data-col="RollUp"] > .nc-virtual-cell`)
.contains(records2.RollUp)
.should("exist");
// formula
mainPage.getCell("Computation", 1).scrollIntoView();
cy.get(
`:nth-child(1) > [data-col="Computation"] > .nc-virtual-cell`
)
.contains(records2.Computation)
.should("exist");
// ltar hm relation
mainPage.getCell("Producer", 1).scrollIntoView();
cy.get(
':nth-child(1) > [data-col="Producer"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(1) > .v-chip__content > .name'
)
.contains(records2.Producer[0])
.should("exist");
cy.get(
':nth-child(1) > [data-col="Producer"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(2) > .v-chip__content > .name'
)
.contains(records2.Producer[1])
.should("exist");
cy.closeTableTab("Film");
});
it("Verify Views & Shared base", () => {
cy.openTableTab("Film", 3);
cy.get('.nc-form-view-item').eq(0)
.click({ force: true })
// Header & description should exist
cy.get(".nc-form")
.find('[placeholder="Form Title"]')
.contains("FormTitle")
.should("exist");
cy.get(".nc-form")
.find('[placeholder="Add form description"]')
.contains("FormDescription")
.should("exist");
// modified column name & help text
cy.get(".nc-field-wrapper").eq(0)
.find('.nc-field-labels')
.contains("DisplayName")
.should('exist')
cy.get(".nc-field-wrapper").eq(0)
.find('.nc-hint')
.contains('HelpText')
.should('exist')
cy.get(".nc-field-wrapper").eq(1)
.find('.nc-field-labels')
.contains("Email")
.should('exist')
// add message
cy.get(".nc-form > .mx-auto")
.find("textarea").then(($element) => {
expect($element[0].value).to.have.string("Thank you for submitting the form!")
})
// submit another form button
cy.get(".nc-form > .mx-auto")
.find('[type="checkbox"]')
.eq(0)
.should('be.checked')
// "New form after 5 seconds" button
cy.get(".nc-form > .mx-auto")
.find('[type="checkbox"]')
.eq(1)
.should('be.checked')
// email me
cy.get(".nc-form > .mx-auto")
.find('[type="checkbox"]')
.eq(2)
.should('not.be.checked')
cy.closeTableTab("Film");
});
it("Verify Webhooks", () => {
cy.openTableTab("Actor", 25);
openWebhook(0)
verifyWebhook({
title: "Webhook-1",
event: "After Insert",
notification: "URL",
type: "POST",
url: "http://localhost:9090/hook",
condition: false
})
cy.get("body").type("{esc}");
openWebhook(1)
verifyWebhook({
title: "Webhook-2",
event: "After Update",
notification: "URL",
type: "POST",
url: "http://localhost:9090/hook",
condition: false
})
cy.get("body").type("{esc}");
openWebhook(2)
verifyWebhook({
title: "Webhook-3",
event: "After Delete",
notification: "URL",
type: "POST",
url: "http://localhost:9090/hook",
condition: false
})
cy.get("body").type("{esc}");
cy.closeTableTab("Actor");
});
it("Pagination", () => {
cy.openTableTab("Actor", 25);
cy.get(".nc-pagination").should("exist");
// verify > pagination option
mainPage.getPagination(">").click();
mainPage
.getPagination(2)
.should("have.class", "v-pagination__item--active");
// verify < pagination option
mainPage.getPagination("<").click();
mainPage
.getPagination(1)
.should("have.class", "v-pagination__item--active");
cy.closeTableTab("Actor");
});
it("Verify Fields, Filter & Sort", () => {
cy.openTableTab("Actor", 25);
cy.get(".nc-grid-view-item").eq(1).click()
cy.get(".nc-grid-header-cell").contains('Name').should("be.visible");
cy.get(".nc-grid-header-cell").contains('Notes').should("be.visible");
cy.get(".nc-grid-header-cell").contains('Attachments').should("not.be.visible");
cy.get(".nc-grid-header-cell").contains('Status').should("be.visible");
cy.get(".nc-grid-header-cell").contains('Film').should("be.visible");
cy.get(".nc-fields-menu-btn").click();
cy.getActiveMenu().find(`[type="checkbox"]`).eq(0).should('be.checked')
cy.getActiveMenu().find(`[type="checkbox"]`).eq(1).should('be.checked')
cy.getActiveMenu().find(`[type="checkbox"]`).eq(2).should('not.be.checked')
cy.getActiveMenu().find(`[type="checkbox"]`).eq(3).should('be.checked')
cy.getActiveMenu().find(`[type="checkbox"]`).eq(4).should('be.checked')
cy.get(".nc-fields-menu-btn").click();
cy.get(".nc-sort-menu-btn").click();
cy.get(".nc-sort-field-select").eq(0)
.contains('Name')
.should("exist");
cy.get(".nc-sort-dir-select").eq(0)
.contains('A -> Z')
.should("exist");
cy.get(".nc-sort-menu-btn").click();
cy.get(".nc-filter-menu-btn").click();
cy.get(".nc-filter-field-select").eq(0)
.contains('Name')
.should("exist");
cy.get(".nc-filter-operation-select").eq(0)
.contains('is like')
.should("exist");
cy.get(".nc-filter-field-select").eq(1)
.contains('Name')
.should("exist");
cy.get(".nc-filter-operation-select").eq(1)
.contains('is like')
.should("exist");
cy.get(".nc-filter-menu-btn").click();
cy.closeTableTab("Actor");
});
it("Views, bt relation", () => {
cy.openTableTab("Producer", 3)
cy.get('.nc-grid-view-item').should('have.length', 4)
cy.get('.nc-form-view-item').should('have.length', 4)
cy.get('.nc-gallery-view-item').should('have.length', 3)
// LinkToAnotherRecord hm relation
mainPage.getCell("FilmRead", 1).scrollIntoView();
cy.get(
':nth-child(1) > [data-col="FilmRead"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(1) > .v-chip__content > .name'
)
.contains('Movie-1')
.should("exist");
cy.closeTableTab("Producer")
})
});
};
genTest("rest", "xcdb");
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Raju Udava <sivadstala@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

36
scripts/cypress/integration/test/quickTest.js

@ -0,0 +1,36 @@
let t9a = require("../common/9a_QuickTest");
const {
setCurrentMode,
} = require("../../support/page_objects/projectConstants");
// use 0 as mode to execute individual files (debug mode, skip pre-configs)
// use 1 mode if noco.db doesnt contain user credentials (full run over GIT)
const nocoTestSuite = (apiType, dbType) => {
setCurrentMode(apiType, dbType);
t9a.genTest(apiType, dbType);
};
nocoTestSuite("rest", "xcdb");
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Raju Udava <sivadstala@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

14
scripts/cypress/plugins/index.js

@ -11,7 +11,7 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const { rmdir } = require("fs");
const { rmdir, copyFile } = require("fs");
// https://stackoverflow.com/questions/61934443/read-excel-files-in-cypress
const readXlsx = require("./read-xlsx");
@ -26,6 +26,18 @@ module.exports = (on, config) => {
// register utility tasks to read and parse Excel files
on("task", {
copyFile() {
console.log("copyFile", __dirname)
return new Promise((resolve, reject) => {
copyFile("./scripts/cypress/fixtures/quickTest/noco_0_91_7.db", "./packages/nocodb/noco.db", (err) => {
if(err) {
console.log(err)
return reject(err)
}
resolve(null);
})
})
},
deleteFolder(folderName) {
console.log("deleting folder %s", folderName);

Loading…
Cancel
Save