Browse Source

Merge pull request #2495 from nocodb/develop

Pre-release 0.92.0
pull/2496/head 0.92.0
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
000ecd8867
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      .github/workflows/ci-cd.yml
  2. 266
      .github/workflows/release-executables.yml
  3. 9
      .github/workflows/release-nightly-dev.yml
  4. 9
      .github/workflows/release-nocodb.yml
  5. 45
      .github/workflows/release-pr.yml
  6. 158
      .github/workflows/release-timely-executables.yml
  7. 11
      package.json
  8. 20
      packages/nc-gui/components/CreateOrEditProject.vue
  9. 27
      packages/nc-gui/components/ProjectTabs.vue
  10. 20
      packages/nc-gui/components/import/ImportFromAirtable.vue
  11. 484
      packages/nc-gui/components/import/JSONImport.vue
  12. 24
      packages/nc-gui/components/import/QuickImport.vue
  13. 1
      packages/nc-gui/components/import/templateParsers/CSVTemplateAdapter.js
  14. 20
      packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js
  15. 6
      packages/nc-gui/components/import/templateParsers/ExcelUrlTemplateAdapter.js
  16. 150
      packages/nc-gui/components/import/templateParsers/JSONTemplateAdapter.js
  17. 21
      packages/nc-gui/components/import/templateParsers/JSONUrlTemplateAdapter.js
  18. 58
      packages/nc-gui/components/import/templateParsers/parserHelpers.js
  19. 3
      packages/nc-gui/components/monaco/MonacoJsonEditor.js
  20. 4
      packages/nc-gui/components/project/spreadsheet/components/Cell.vue
  21. 51
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  22. 31
      packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue
  23. 69
      packages/nc-gui/components/project/spreadsheet/components/cell/DurationCell.vue
  24. 72
      packages/nc-gui/components/project/spreadsheet/components/editColumn/DurationOptions.vue
  25. 5
      packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue
  26. 139
      packages/nc-gui/components/project/spreadsheet/components/editableCell/DurationCell.vue
  27. 3
      packages/nc-gui/components/project/spreadsheet/components/editableCell/TimePickerCell.vue
  28. 29
      packages/nc-gui/components/project/spreadsheet/mixins/cell.js
  29. 28
      packages/nc-gui/components/project/spreadsheet/views/FormView.vue
  30. 192
      packages/nc-gui/helpers/durationHelper.js
  31. 288
      packages/nc-gui/lang/it_IT.json
  32. 44
      packages/nc-gui/package-lock.json
  33. 2
      packages/nc-gui/package.json
  34. 7
      packages/nc-gui/pages/project/id.vue
  35. 10
      packages/nc-gui/pages/project/xcdb.vue
  36. 98
      packages/nc-gui/pages/user/authentication/passwordValidateMixin.js
  37. 5
      packages/nc-gui/pages/user/authentication/signin.vue
  38. 16
      packages/nc-plugin/package-lock.json
  39. 14
      packages/noco-blog/package-lock.json
  40. 28
      packages/noco-docs-prev/package-lock.json
  41. 24
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  42. 42
      packages/noco-docs/content/en/engineering/timely-build.md
  43. 21
      packages/noco-docs/content/en/getting-started/installation.md
  44. 2
      packages/noco-docs/content/en/setup-and-usages/dashboard.md
  45. 44
      packages/noco-docs/content/en/setup-and-usages/primary-key.md
  46. 2
      packages/noco-docs/content/en/setup-and-usages/primary-value.md
  47. 28
      packages/noco-docs/package-lock.json
  48. 6
      packages/noco-i18n/package-lock.json
  49. 2
      packages/nocodb-sdk/src/index.ts
  50. 56
      packages/nocodb-sdk/src/lib/Api.ts
  51. 30
      packages/nocodb-sdk/src/lib/TemplateGenerator.ts
  52. 41
      packages/nocodb-sdk/src/lib/passwordHelpers.ts
  53. 7
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  54. 2
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  55. 4
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  56. 2
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  57. 3
      packages/nocodb/README.md
  58. 58
      packages/nocodb/package-lock.json
  59. 5
      packages/nocodb/package.json
  60. 10
      packages/nocodb/src/__tests__/restv2.test.ts
  61. 17
      packages/nocodb/src/lib/Noco.ts
  62. 86
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  63. 12
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts
  64. 2
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts
  65. 9
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/sanitize.ts
  66. 3
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/sortV2.ts
  67. 5
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaPg.ts
  68. 4
      packages/nocodb/src/lib/jobs/RedisJobsMgr.ts
  69. 12
      packages/nocodb/src/lib/meta/api/columnApis.ts
  70. 12
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts
  71. 6
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  72. 13
      packages/nocodb/src/lib/meta/api/projectApis.ts
  73. 4
      packages/nocodb/src/lib/meta/api/projectUserApis.ts
  74. 21
      packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts
  75. 5
      packages/nocodb/src/lib/meta/api/userApi/helpers.ts
  76. 236
      packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts
  77. 25
      packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
  78. 2
      packages/nocodb/src/lib/meta/api/userApi/ui/auth/emailVerify.ts
  79. 74
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  80. 33
      packages/nocodb/src/lib/meta/api/utilApis.ts
  81. 66
      packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts
  82. 5
      packages/nocodb/src/lib/meta/helpers/stringHelpers.ts
  83. 1
      packages/nocodb/src/lib/models/Filter.ts
  84. 10
      packages/nocodb/src/lib/models/Model.ts
  85. 4
      packages/nocodb/src/lib/models/Plugin.ts
  86. 3
      packages/nocodb/src/lib/models/User.ts
  87. 29
      packages/nocodb/src/lib/models/View.ts
  88. 23
      packages/nocodb/tests/export-import/ReadMe.md
  89. 6
      packages/nocodb/tests/export-import/config.json
  90. 297
      packages/nocodb/tests/export-import/exportSchema.js
  91. 537
      packages/nocodb/tests/export-import/importSchema.js
  92. 4759
      packages/nocodb/tests/pg-cy-quick/01-cy-quick.sql
  93. 5
      scripts/cypress/cypress.json
  94. 17
      scripts/cypress/docker-compose-pg-cy-quick.yml
  95. BIN
      scripts/cypress/fixtures/quickTest/noco_0_91_7.db
  96. 4
      scripts/cypress/integration/common/1a_table_operations.js
  97. 4
      scripts/cypress/integration/common/2b_table_with_m2m_column.js
  98. 275
      scripts/cypress/integration/common/3e_duration_column.js
  99. 2
      scripts/cypress/integration/common/4e_form_view_share.js
  100. 20
      scripts/cypress/integration/common/4f_grid_view_share.js
  101. Some files were not shown because too many files have changed in this diff Show More

36
.github/workflows/ci-cd.yml

@ -49,7 +49,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -61,7 +60,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:
@ -94,7 +93,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -106,7 +104,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:
@ -139,7 +137,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -151,7 +148,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:
@ -184,7 +181,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -196,7 +192,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:
@ -229,7 +225,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -241,7 +236,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:
@ -274,7 +269,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -286,7 +280,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:
@ -319,7 +313,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -331,7 +324,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:
@ -364,7 +357,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -376,7 +368,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:
@ -409,7 +401,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -421,7 +412,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:
@ -454,7 +445,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -466,7 +456,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:
@ -499,7 +489,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -511,7 +500,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:
@ -544,7 +533,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -556,7 +544,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:

266
.github/workflows/release-executables.yml

@ -0,0 +1,266 @@
name: "Release : Executables"
on:
# Triggered manually
workflow_dispatch:
inputs:
tag:
description: "Tag name"
required: true
# Triggered by release-nocodb.yml
workflow_call:
inputs:
tag:
description: "Tag name"
required: true
type: string
secrets:
NC_GITHUB_TOKEN:
required: true
jobs:
build-executables:
runs-on: ubuntu-latest
steps:
# Get the latest draft release for asset upload url
- uses: cardinalby/git-get-release-action@v1
id: get_release
env:
GITHUB_TOKEN: ${{ secrets.NC_GITHUB_TOKEN }}
with:
latest: 1
draft: true
- uses: actions/checkout@v3
- 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 }}-
# for building images for all platforms these libraries are required in Linux
- name: Install QEMU and ldid
run: |
sudo apt update
# 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
- uses: actions/setup-node@v3
with:
node-version: 16
- name : Install nocodb, other dependencies and build executables
run: |
cd ./scripts/pkg-executable
# Install nocodb version based on provided tag name
npm i -E nocodb@$TAG
# 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/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
# build executables
npm run build
ls ./dist
# Move macOS executables for signing
mkdir ./mac-dist
mv ./dist/Noco-macos-arm64 ./mac-dist/
mv ./dist/Noco-macos-x64 ./mac-dist/
- name: Upload win-arm64 build to asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-win-arm64.exe
asset_name: Noco-win-arm64
asset_content_type: application/octet-stream
- name: Upload win-x64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-win-x64.exe
asset_name: Noco-win-x64
asset_content_type: application/octet-stream
- name: Upload linux-arm64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-linux-arm64
asset_name: Noco-linux-arm64
asset_content_type: application/octet-stream
- name: Upload linux-x64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-linux-x64
asset_name: Noco-linux-x64
asset_content_type: application/octet-stream
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: scripts/pkg-executable/mac-dist
retention-days: 1
outputs:
upload_url: ${{ steps.get_release.outputs.upload_url }}
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: scripts/pkg-executable/mac-dist
- name: Sign macOS executables
run: |
/usr/bin/codesign --force -s - ./scripts/pkg-executable/mac-dist/Noco-macos-arm64 -v
/usr/bin/codesign --force -s - ./scripts/pkg-executable/mac-dist/Noco-macos-x64 -v
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: scripts/pkg-executable/mac-dist
retention-days: 1
publish-mac-executables-and-homebrew:
needs: [sign-mac-executables,build-executables]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: scripts/pkg-executable/mac-dist
- uses: actions/checkout@v3
with:
path: 'homebrew-nocodb'
token: ${{ secrets.NC_GITHUB_TOKEN }}
repository: 'nocodb/homebrew-nocodb'
fetch-depth: 0
- name: Compress files and calculate checksum
run: |
cd ./scripts/pkg-executable
cp ./mac-dist/Noco-macos-x64 ./mac-dist/nocodb
tar -czf ./mac-dist/nocodb.tar.gz ./mac-dist/nocodb
rm ./mac-dist/nocodb
echo "::set-output name=CHECKSUM::$(shasum -a 256 ./mac-dist/nocodb.tar.gz | awk '{print $1}')"
id: compress
- name: Upload macos-x64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build-executables.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/mac-dist/Noco-macos-x64
asset_name: Noco-macos-x64
asset_content_type: application/octet-stream
- name: Upload macos-arm64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build-executables.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/mac-dist/Noco-macos-arm64
asset_name: Noco-macos-arm64
asset_content_type: application/octet-stream
- name: Upload macos compressed build(for homebrew) to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build-executables.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/mac-dist/nocodb.tar.gz
asset_name: nocodb.tar.gz
asset_content_type: application/octet-stream
- name: Generate Homebrew Formula class and push
run: |
FORMULA_CLASS_STR=$(cat << EOF
class Nocodb < Formula
desc "Get Human Readable file size information. - CLI"
homepage "https://github.com/nocodb/nocodb"
url "https://github.com/nocodb/nocodb/releases/download/${{ github.event.inputs.tag || inputs.tag }}/nocodb.tar.gz"
sha256 "${{ steps.compress.outputs.CHECKSUM }}"
license "MIT"
version "${{ github.event.inputs.tag || inputs.tag }}"
def install
bin.install "nocodb"
end
end
EOF
)
cd ./homebrew-nocodb
printf "$FORMULA_CLASS_STR" > ./Formula/nocodb.rb
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git commit ./Formula/nocodb.rb -m "Automatic publish"
git push

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-timely-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]

9
.github/workflows/release-nocodb.yml

@ -94,6 +94,15 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [release-draft-note, process-input]
uses: ./.github/workflows/release-executables.yml
with:
tag: ${{ needs.process-input.outputs.target_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Close all issues with target tags 'Fixed' & 'Resolved'
close-fixed-issues:
needs: [release-docker, process-input]

45
.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-timely-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,42 @@ 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 "$_" \
&& 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 "$_" \
&& 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

158
.github/workflows/release-timely-executables.yml

@ -0,0 +1,158 @@
name: "Release : Timely 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: |
sudo apt update
# 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/
- 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: 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

11
package.json

@ -15,11 +15,12 @@
"scripts": {
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build",
"install:common": "cd ./packages/nocodb; npm install ../nocodb-sdk; cd ../nc-gui; npm install ../nocodb-sdk",
"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: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",
"start:api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;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": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:xcdb-api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; 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": "npm run build:common ; cd ./packages/nc-gui; npm install ../nocodb-sdk; npm install; npm run dev",
"cypress:run": "cypress run --config-file ./scripts/cypress/cypress.json",
"cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json",
"cypress:clear": "cypress cache clear",

20
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
@ -453,14 +453,14 @@
"
/>
</v-col>
<!-- todo : Schema name -->
<!-- Schema name -->
<v-col
v-if="db.client === 'mssql' || db.client === 'pg'"
cols="4"
class="py-0"
>
<v-text-field
v-model="schema"
v-model="db.searchPath[0]"
:disabled="edit && enableDbEdit < 2"
class="body-2 database-field"
:rules="form.requiredRule"
@ -891,7 +891,6 @@ export default {
layout: 'empty',
data() {
return {
schema: 'public',
testSuccess: false,
projectCreated: false,
allSchemas: false,
@ -963,7 +962,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']
},
@ -1539,9 +1541,7 @@ export default {
this.projectReloading = true
const con = projectJson.envs._noco.db[0]
if (con.client === 'pg' || con.client === 'mssql') {
con.searchPath = [this.schema]
} else if ('searchPath' in con) {
if (con.client !== 'pg' && con.client !== 'mssql' && 'searchPath' in con) {
delete con.searchPath
}
@ -1892,9 +1892,9 @@ export default {
},
onDatabaseTypeChanged(client, db1, index, env) {
if (this.databaseNames[client] === 'mssql') {
this.schema = 'dbo'
this.project.envs[env].db[index].searchPath[0] = 'dbo'
} else if (this.databaseNames[client] === 'pg') {
this.schema = 'public'
this.project.envs[env].db[index].searchPath[0] = 'public'
}
for (const env in this.project.envs) {

27
packages/nc-gui/components/ProjectTabs.vue

@ -315,6 +315,21 @@
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('jsonImport')"
v-t="['a:actions:import-json']"
@click="jsonImportModal = true"
>
<v-list-item-title>
<v-icon small>
mdi-code-json
</v-icon>
<span class="caption">
<!-- TODO: i18n -->
JSON file
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('excelQuickImport')"
v-t="['a:actions:import-excel']"
@ -370,6 +385,13 @@
@closeModal="quickImportModal = false"
/>
<!-- Import From JSON string / file -->
<json-import
v-model="jsonImportModal"
hide-label
@closeModal="jsonImportModal = false"
/>
<import-from-airtable v-if="airtableImportModal" v-model="airtableImportModal" />
</v-container>
</template>
@ -404,9 +426,11 @@ import GlobalAcl from '~/components/GlobalAcl'
import AuditTab from '~/components/project/AuditTab'
import QuickImport from '~/components/import/QuickImport'
import ImportFromAirtable from '~/components/import/ImportFromAirtable'
import JsonImport from '~/components/import/JSONImport'
export default {
components: {
JsonImport,
ImportFromAirtable,
SwaggerClient,
// Screensaver,
@ -447,7 +471,8 @@ export default {
showScreensaver: false,
quickImportModal: false,
quickImportType: '',
airtableImportModal: false
airtableImportModal: false,
jsonImportModal: false
}
},
methods: {

20
packages/nc-gui/components/import/ImportFromAirtable.vue

@ -8,7 +8,6 @@
<div
v-t="['c:airtable-import:turbo-mode']"
class="ml-2 mt-3 title pointer nc-btn-enable-turbo"
@click="enableTurbo"
>
🚀
</div>
@ -86,6 +85,13 @@
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncViews"
class="caption"
label="Import Secondary Views"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncRollup"
class="caption"
@ -225,7 +231,7 @@ export default {
apiKey: '',
shareId: '',
options: {
syncViews: false,
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
@ -327,7 +333,7 @@ export default {
apiKey: '',
shareId: '',
options: {
syncViews: false,
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
@ -350,10 +356,10 @@ export default {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
},
enableTurbo() {
this.$set(this.syncSource.details.options, 'syncViews', true)
this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
},
// enableTurbo() {
// this.$set(this.syncSource.details.options, 'syncViews', true)
// this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
// },
migrateSync(src) {
if (!src.details?.options) {
src.details.options = {

484
packages/nc-gui/components/import/JSONImport.vue

@ -0,0 +1,484 @@
<template>
<div :class="{'pt-10':!hideLabel}">
<v-dialog v-model="dropOrUpload" max-width="600">
<v-card max-width="600">
<v-tabs height="30">
<v-tab>
<v-icon small class="mr-1">
mdi-file-upload-outline
</v-icon>
<span class="caption text-capitalize">Upload</span>
</v-tab>
<!-- <v-tab>-->
<!-- <v-icon small class="mr-1">
mdi-link-variant
</v-icon>
<span class="caption text-capitalize">URL</span>
</v-tab>-->
<v-tab>
<v-icon small class="mr-1">
mdi-link-variant
</v-icon>
<span class="caption text-capitalize">String</span>
</v-tab>
<v-tab-item>
<div class="nc-json-import-tab-item ">
<div
class="nc-droppable d-flex align-center justify-center flex-column"
:style="{
background : dragOver ? '#7772' : ''
}"
@click="$refs.file.click()"
@drop.prevent="dropHandler"
@dragover.prevent="dragOver = true"
@dragenter.prevent="dragOver = true"
@dragexit="dragOver = false"
@dragleave="dragOver = false"
@dragend="dragOver = false"
>
<x-icon :color="['primary','grey']" size="50">
mdi-file-plus-outline
</x-icon>
<p class="title mb-1 mt-2">
<!-- Select File to Upload-->
{{ $t('msg.info.upload') }}
</p>
<p class="grey--text mb-1">
<!-- or drag and drop file-->
{{ $t('msg.info.upload_sub') }}
</p>
<p v-if="quickImportType == 'excel'" class="caption grey--text">
<!-- Supported: .xls, .xlsx, .xlsm, .ods, .ots -->
{{ $t('msg.info.excelSupport') }}
</p>
</div>
</div>
</v-tab-item>
<!-- <v-tab-item>
<div class="nc-json-import-tab-item align-center">
<div class="pa-4 d-100 h-100">
<v-form ref="form" v-model="valid">
<div class="d-flex">
&lt;!&ndash; todo: i18n label&ndash;&gt;
<v-text-field
v-model="url"
hide-details="auto"
type="url"
label="Enter JSON file url"
class="caption"
outlined
dense
: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-btn v-t="['c:project:create:json:load-url']" class="ml-3" color="primary" @click="loadUrl">
&lt;!&ndash;Load&ndash;&gt;
{{ $t('general.load') }}
</v-btn>
</div>
</v-form>
</div>
</div>
</v-tab-item>-->
<v-tab-item>
<div class="nc-json-import-tab-item align-center">
<div class="pa-4 d-100 h-100">
<v-form ref="form" v-model="valid">
<div class="nc-json-editor-wrapper">
<v-btn small class="nc-json-format-btn" @click="formatJson">
Format
</v-btn>
<!--label="Enter excel file url"-->
<monaco-json-editor
ref="editor"
v-model="jsonString"
style="height:320px"
/>
<div class="text-center mt-4">
<v-btn v-t="['c:project:create:excel:load-url']" class="ml-3" color="primary" @click="loadJsonString">
<!--Load-->
{{ $t('general.load') }}
</v-btn>
</div>
</div>
</v-form>
</div>
</div>
</v-tab-item>
</v-tabs>
<div class="px-4 pb-2">
<div class="d-flex">
<v-spacer />
<span class="caption pointer grey--text" @click="showMore = !showMore">
{{ showMore ? $t('general.hideAll') : $t('general.showMore') }}
<v-icon small color="grey lighten-1">mdi-menu-{{ showMore ? 'up' : 'down' }}</v-icon>
</span>
</div>
<div class="mb-2 pt-2 nc-json-import-options" :style="{ maxHeight: showMore ? '200px' : '0'}">
<p />
<!--hint="# of rows to parse to infer data type"-->
<v-text-field
v-model="parserConfig.maxRowsToParse"
style="max-width: 250px"
class="caption mx-auto"
dense
persistent-hint
:hint="$t('msg.info.footMsg')"
outlined
type="number"
/>
<v-checkbox
v-model="parserConfig.normalizeNested"
style="width: 250px"
class="mx-auto mb-2"
dense
hide-details
>
<template #label>
<span class="caption">Flatten nested</span>
<v-tooltip bottom position-y="">
<template #activator="{ on }">
<v-icon small class="ml-1" v-on="on">
mdi-information-outline
</v-icon>
</template>
<div class="caption" style="width: 260px">
If flatten nested option is set it will flatten nested object as root level property. In normal case nested object will treat as JSON column.
<br>
<br>
For example the following input: <code class="caption font-weight-bold">{
"prop1": {
"prop2": "value"
},
"prop3": "value",
"prop4": 1
}</code> will treat as:
<code class="caption font-weight-bold">{
"prop1_prop2": "value",
"prop3": "value",
"prop4": 1
}</code>
</div>
</v-tooltip>
</template>
</v-checkbox>
<v-checkbox
v-model="parserConfig.importData"
style="width: 250px"
class="mx-auto mb-2"
dense
hide-details
>
<template #label>
<span class="caption">Import data</span>
</template>
</v-checkbox>
</div>
</div>
</v-card>
</v-dialog>
<v-tooltip bottom>
<template #activator="{on}">
<input
ref="file"
class="nc-json-import-input"
type="file"
style="display: none"
accept=".json"
@change="_change($event)"
>
<v-btn
v-if="!hideLabel"
small
outlined
v-on="on"
@click="$refs.file.click()"
>
<v-icon small class="mr-1">
mdi-file-excel-outline
</v-icon>
<!--Import-->
{{ $t('activity.import') }}
</v-btn>
</template>
<span class="caption">Create template from JSON</span>
</v-tooltip>
<v-dialog v-if="templateData" v-model="templateEditorModal" max-width="1000">
<v-card class="pa-6" min-width="500">
<template-editor :project-template.sync="templateData" json-import :quick-import-type="quickImportType">
<template #toolbar="{valid}">
<h3 class="mt-2 grey--text">
<span>
JSON Import
</span>
</h3>
<v-spacer />
<v-spacer />
<create-project-from-template-btn
:template-data="templateData"
:import-data="importData"
:import-to-project="importToProject"
json-import
:valid="valid"
create-gql-text="Import as GQL Project"
create-rest-text="Import as REST Project"
@closeModal="$emit('closeModal'),templateEditorModal = false"
>
<!--Import Excel-->
<span v-if="quickImportType === 'excel'">
{{ $t('activity.importExcel') }}
</span>
<!--Import CSV-->
<span v-if="quickImportType === 'csv'">
{{ $t('activity.importCSV') }}
</span>
</create-project-from-template-btn>
</template>
</template-editor>
</v-card>
</v-dialog>
</div>
</template>
<script>
import TemplateEditor from '~/components/templates/Editor'
import CreateProjectFromTemplateBtn from '~/components/templates/CreateProjectFromTemplateBtn'
import MonacoJsonEditor from '~/components/monaco/MonacoJsonEditor'
import JSONTemplateAdapter from '~/components/import/templateParsers/JSONTemplateAdapter'
import JSONUrlTemplateAdapter from '~/components/import/templateParsers/JSONUrlTemplateAdapter'
export default {
name: 'JsonImport',
components: { MonacoJsonEditor, CreateProjectFromTemplateBtn, TemplateEditor },
props: {
hideLabel: Boolean,
value: Boolean,
importToProject: Boolean,
quickImportType: String
},
data() {
return {
templateEditorModal: false,
valid: null,
templateData: null,
importData: null,
dragOver: false,
url: '',
showMore: false,
parserConfig: {
maxRowsToParse: 500,
normalizeNested: true,
importData: true
},
filename: '',
jsonString: '',
errorMessages: {
ipBlockList: 'IP Not allowed!',
importJSON: 'Target file is not an accepted file type. The accepted file type is .json!'
}
}
},
computed: {
dropOrUpload: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
}
},
tables() {
return this.$store.state.project.tables || []
}
},
mounted() {
if (this.$route && this.$route.query && this.$route.query.excelUrl) {
this.url = this.$route.query.excelUrl
this.loadUrl()
}
},
methods: {
formatJson() {
console.log(this.$refs.editor)
this.$refs.editor.format()
},
selectFile() {
this.$refs.file.files = null
this.$refs.file.click()
},
_change(event) {
const files = event.target.files
if (files && files[0]) {
this._file(files[0])
event.target.value = ''
}
},
async _file(file) {
this.templateData = null
this.importData = null
this.$store.commit('loader/MutMessage', 'Loading excel file')
let i = 0
const int = setInterval(() => {
this.$store.commit('loader/MutMessage', `Loading excel file${'.'.repeat(++i % 4)}`)
}, 1000)
this.dropOrUpload = false
const reader = new FileReader()
this.filename = file.name
reader.onload = async(e) => {
const ab = e.target.result
await this.parseAndExtractData('file', ab, file.name)
this.$store.commit('loader/MutMessage', null)
clearInterval(int)
}
const handleEvent = (event) => {
this.$store.commit('loader/MutMessage', `${event.type}: ${event.loaded} bytes transferred`)
}
reader.addEventListener('progress', handleEvent)
reader.onerror = (e) => {
console.log('error', e)
this.$store.commit('loader/MutClear')
}
reader.readAsText(file)
},
async parseAndExtractData(type, val, name) {
try {
let templateGenerator
this.templateData = null
this.importData = null
switch (type) {
case 'file':
templateGenerator = new JSONTemplateAdapter(name, val, this.parserConfig)
break
case 'url':
templateGenerator = new JSONUrlTemplateAdapter(val, this.$store, this.parserConfig, this.$api)
break
case 'string':
templateGenerator = new JSONTemplateAdapter(name, val, this.parserConfig)
break
}
await templateGenerator.init()
templateGenerator.parse()
this.templateData = templateGenerator.getTemplate()
this.templateData.tables[0].table_name = this.populateUniqueTableName()
this.importData = templateGenerator.getData()
this.templateEditorModal = true
} catch (e) {
console.log(e)
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000)
}
},
dropHandler(ev) {
this.dragOver = false
let file
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
if (ev.dataTransfer.items.length && ev.dataTransfer.items[0].kind === 'file') {
file = ev.dataTransfer.items[0].getAsFile()
}
} else if (ev.dataTransfer.files.length) {
file = ev.dataTransfer.files[0]
}
if (!file) {
return
}
if (!/.*\.json/.test(file.name)) {
return this.$toast.error(this.errorMessages.importJSON).goAway(3000)
}
this._file(file)
},
dragOverHandler(ev) {
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault()
},
populateUniqueTableName() {
let c = 1
while (this.tables.some(t => t.title === `Sheet${c}`)) { c++ }
return `Sheet${c}`
},
async loadUrl() {
if ((this.$refs.form && !this.$refs.form.validate()) || !this.url) {
return
}
this.$store.commit('loader/MutMessage', 'Loading json file from url')
let i = 0
const int = setInterval(() => {
this.$store.commit('loader/MutMessage', `Loading json file${'.'.repeat(++i % 4)}`)
}, 1000)
this.dropOrUpload = false
await this.parseAndExtractData('url', this.url, '')
clearInterval(int)
this.$store.commit('loader/MutClear')
},
async loadJsonString() {
await this.parseAndExtractData('string', this.jsonString)
this.$store.commit('loader/MutClear')
}
}
}
</script>
<style scoped>
.nc-droppable {
width: 100%;
min-height: 200px;
border-radius: 4px;
border: 2px dashed #ddd;
}
.nc-json-import-tab-item {
min-height: 400px;
padding: 20px;
display: flex;
align-items: stretch;
width: 100%;
}
.nc-json-import-options {
transition: .4s max-height;
overflow: hidden;
}
.nc-json-editor-wrapper{
position: relative;
}
.nc-json-format-btn{
position:absolute;
right:4px;
top:4px;
z-index:9;
}
</style>

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)

1
packages/nc-gui/components/import/templateParsers/CSVTemplateAdapter.js

@ -1,6 +1,5 @@
import Papaparse from 'papaparse'
import TemplateGenerator from '~/components/import/templateParsers/TemplateGenerator'
export default class CSVTemplateAdapter extends TemplateGenerator {
constructor(name, data) {
super()

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

150
packages/nc-gui/components/import/templateParsers/JSONTemplateAdapter.js

@ -0,0 +1,150 @@
import { TemplateGenerator, UITypes } from 'nocodb-sdk'
import {
extractMultiOrSingleSelectProps,
getCheckboxValue,
isCheckboxType, isDecimalType, isEmailType,
isMultiLineTextType, isUrlType
} from '~/components/import/templateParsers/parserHelpers'
const jsonTypeToUidt = {
number: UITypes.Number,
string: UITypes.SingleLineText,
date: UITypes.DateTime,
boolean: UITypes.Checkbox,
object: UITypes.JSON
}
const extractNestedData = (obj, path) => path.reduce((val, key) => val && val[key], obj)
export default class JSONTemplateAdapter extends TemplateGenerator {
constructor(name = 'test', data, parserConfig = {}) {
super()
this.config = {
maxRowsToParse: 500,
...parserConfig
}
this.name = name
this._jsonData = typeof data === 'string' ? JSON.parse(data) : data
this.project = {
title: this.name,
tables: []
}
this.data = {}
}
async init() {
}
parseData() {
this.columns = this.csv.meta.fields
this.data = this.csv.data
}
getColumns() {
return this.columns
}
getData() {
return this.data
}
get jsonData() {
return Array.isArray(this._jsonData) ? this._jsonData : [this._jsonData]
}
parse() {
const jsonData = this.jsonData
const tn = 'table'
const table = { table_name: tn, ref_table_name: tn, columns: [] }
this.data[tn] = []
for (const col of Object.keys(jsonData[0])) {
const columns = this._parseColumn([col], jsonData)
table.columns.push(...columns)
}
if (this.config.importData) { this._parseTableData(table) }
this.project.tables.push(table)
}
getTemplate() {
return this.project
}
_parseColumn(path = [], jsonData = this.jsonData, firstRowVal = path.reduce((val, k) => val && val[k], this.jsonData[0])) {
const columns = []
// parse nested
if (firstRowVal && typeof firstRowVal === 'object' && !Array.isArray(firstRowVal) && this.config.normalizeNested) {
for (const key of Object.keys(firstRowVal)) {
const normalizedNestedColumns = this._parseColumn([...path, key], this.jsonData, firstRowVal[key])
columns.push(...normalizedNestedColumns)
}
} else {
const cn = path.join('_').replace(/\W/g, '_').trim()
const column = {
column_name: cn,
ref_column_name: cn,
path
}
column.uidt = jsonTypeToUidt[typeof firstRowVal] || UITypes.SingleLineText
const colData = jsonData.map(r => extractNestedData(r, path))
Object.assign(column, this._getColumnUIDTAndMetas(colData, column.uidt))
columns.push(column)
}
return columns
}
_getColumnUIDTAndMetas(colData, defaultType) {
const colProps = { uidt: defaultType }
// todo: optimize
if (colProps.uidt === UITypes.SingleLineText) {
// check for long text
if (isMultiLineTextType(colData)) {
colProps.uidt = UITypes.LongText
} if (isEmailType(colData)) {
colProps.uidt = UITypes.Email
} if (isUrlType(colData)) {
colProps.uidt = UITypes.URL
} else {
const checkboxType = isCheckboxType(colData)
if (checkboxType.length === 1) {
colProps.uidt = UITypes.Checkbox
} else {
Object.assign(colProps, extractMultiOrSingleSelectProps(colData))
}
}
} else if (colProps.uidt === UITypes.Number) {
if (isDecimalType(colData)) {
colProps.uidt = UITypes.Decimal
}
}
return colProps
}
_parseTableData(tableMeta) {
for (const row of this.jsonData) {
const rowData = {}
for (let i = 0; i < tableMeta.columns.length; i++) {
const value = extractNestedData(row, tableMeta.columns[i].path || [])
if (tableMeta.columns[i].uidt === UITypes.Checkbox) {
rowData[tableMeta.columns[i].ref_column_name] = getCheckboxValue(value)
} else if (tableMeta.columns[i].uidt === UITypes.SingleSelect || tableMeta.columns[i].uidt === UITypes.MultiSelect) {
rowData[tableMeta.columns[i].ref_column_name] = (value || '').toString().trim() || null
} else if (tableMeta.columns[i].uidt === UITypes.JSON) {
rowData[tableMeta.columns[i].ref_column_name] = JSON.stringify(value)
} else {
// toto: do parsing if necessary based on type
rowData[tableMeta.columns[i].column_name] = value
}
}
this.data[tableMeta.ref_table_name].push(rowData)
// rowIndex++
}
}
}

21
packages/nc-gui/components/import/templateParsers/JSONUrlTemplateAdapter.js

@ -0,0 +1,21 @@
import JSONTemplateAdapter from '~/components/import/templateParsers/JSONTemplateAdapter'
export default class JSONUrlTemplateAdapter extends JSONTemplateAdapter {
constructor(url, $store, parserConfig, $api) {
const name = url.split('/').pop()
super(name, null, parserConfig)
this.url = url
this.$api = $api
this.$store = $store
}
async init() {
const data = await this.$api.utils.axiosRequestMake({
apiMeta: {
url: this.url
}
})
this._jsonData = data
await super.init()
}
}

58
packages/nc-gui/components/import/templateParsers/parserHelpers.js

@ -1,3 +1,6 @@
import { UITypes } from 'nocodb-sdk'
import { isEmail, isValidURL } from '~/helpers'
const booleanOptions = [
{ checked: true, unchecked: false },
{ x: true, '': false },
@ -11,14 +14,24 @@ const booleanOptions = [
{ '✔': true, '': false },
{ enabled: true, disabled: false },
{ on: true, off: false },
{ done: true, '': false }
{ done: true, '': false },
{ true: true, false: false }
]
const aggBooleanOptions = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {})
export const isCheckboxType = (values, col = '') => {
const getColVal = (row, col = null) => {
return row && col ? row[col] : row
}
export const isCheckboxType = (values, col = null) => {
let options = booleanOptions
for (let i = 0; i < values.length; i++) {
let val = col ? values[i][col] : values[i]
val = val === null || val === undefined ? '' : val
const val = getColVal(values[i], col)
if (val === null || val === undefined || val.toString().trim() === '') {
continue
}
options = options.filter(v => val in v)
if (!options.length) {
return false
@ -29,3 +42,40 @@ export const isCheckboxType = (values, col = '') => {
export const getCheckboxValue = (value) => {
return value && aggBooleanOptions[value]
}
export const isMultiLineTextType = (values, col = null) => {
return values.some(r =>
(getColVal(r, col) || '').toString().match(/[\r\n]/) ||
(getColVal(r, col) || '').toString().length > 255)
}
export const extractMultiOrSingleSelectProps = (colData) => {
const colProps = {}
if (colData.some(v => v && (v || '').toString().includes(','))) {
let flattenedVals = colData.flatMap(v => v ? v.toString().trim().split(/\s*,\s*/) : [])
const uniqueVals = flattenedVals = flattenedVals
.filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase()))
if (flattenedVals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(flattenedVals.length / 2)) {
colProps.uidt = UITypes.MultiSelect
colProps.dtxp = `'${uniqueVals.join("','")}'`
}
} else {
const uniqueVals = colData.map(v => (v || '').toString().trim()).filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase()))
if (colData.length > uniqueVals.length && uniqueVals.length <= Math.ceil(colData.length / 2)) {
colProps.uidt = UITypes.SingleSelect
colProps.dtxp = `'${uniqueVals.join("','")}'`
}
}
return colProps
}
export const isDecimalType = colData => colData.some((v) => {
return v && parseInt(+v) !== +v
})
export const isEmailType = colData => !colData.some((v) => {
return v && !isEmail(v)
})
export const isUrlType = colData => !colData.some((v) => {
return v && !isValidURL(v)
})

3
packages/nc-gui/components/monaco/MonacoJsonEditor.js

@ -83,6 +83,9 @@ export default {
},
methods: {
format() {
this.editor.getAction('editor.action.formatDocument').run()
},
resizeLayout() {
this.editor.layout();
},

4
packages/nc-gui/components/project/spreadsheet/components/Cell.vue

@ -17,6 +17,7 @@
<date-time-cell v-else-if="isDateTime" :value="value" />
<time-cell v-else-if="isTime" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" read-only />
<duration-cell v-else-if="isDuration" :column="column" :value="value" read-only />
<rating-cell v-else-if="isRating" :value="value" read-only />
<currency-cell v-else-if="isCurrency" :value="value" :column="column" />
@ -37,10 +38,11 @@ import BooleanCell from '~/components/project/spreadsheet/components/cell/Boolea
import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import CurrencyCell from '@/components/project/spreadsheet/components/cell/CurrencyCell'
import DurationCell from '@/components/project/spreadsheet/components/cell/DurationCell'
export default {
name: 'TableCell',
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell, CurrencyCell },
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell, CurrencyCell, DurationCell },
mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: {

51
packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue

@ -172,7 +172,6 @@
:column="newColumn"
:meta="meta"
/>
<v-col
v-if="accordion"
cols="12"
@ -210,7 +209,31 @@
/>
</v-col>
<template v-if="newColumn.uidt !== 'Formula'">
<template v-if="newColumn.uidt === 'Formula'">
<v-col cols="12">
<formula-options
ref="formula"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
:sql-ui="sqlUi"
v-on="$listeners"
/>
</v-col>
</template>
<template v-else-if="newColumn.uidt === 'Duration'">
<v-col cols="12">
<duration-options
v-model="newColumn.meta"
:column="newColumn"
:meta="meta"
/>
</v-col>
</template>
<template v-else>
<v-col v-if="isLookup" cols="12">
<lookup-options
ref="lookup"
@ -497,21 +520,6 @@
</v-col>
</template>
</template>
<template v-else>
<v-col cols="12">
<formula-options
ref="formula"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
:sql-ui="sqlUi"
v-on="$listeners"
/>
</v-col>
</template>
</v-row>
</v-col>
</template>
@ -572,6 +580,7 @@ import { validateColumnName } from '~/helpers'
import RatingOptions from '~/components/project/spreadsheet/components/editColumn/RatingOptions'
import CheckboxOptions from '~/components/project/spreadsheet/components/editColumn/CheckboxOptions'
import CurrencyOptions from '@/components/project/spreadsheet/components/editColumn/CurrencyOptions'
import DurationOptions from '@/components/project/spreadsheet/components/editColumn/DurationOptions'
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
@ -587,7 +596,8 @@ export default {
DlgLabelSubmitCancel,
RelationOptions,
CustomSelectOptions,
CurrencyOptions
CurrencyOptions,
DurationOptions
},
props: {
nodes: Object,
@ -617,7 +627,8 @@ export default {
UITypes.Lookup,
UITypes.Rollup,
UITypes.SpecificDBType,
UITypes.Formula
UITypes.Formula,
UITypes.Duration
].includes(this.newColumn && this.newColumn.uidt)
},
uiTypes() {
@ -631,7 +642,7 @@ export default {
]
},
isEditDisabled() {
return this.editColumn && this.sqlUi === SqliteUi
return this.editColumn && this.sqlUi === SqliteUi && this.column.uidt !== UITypes.Duration
},
isSQLite() {
return this.sqlUi === SqliteUi

31
packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue

@ -35,6 +35,16 @@
v-on="$listeners"
/>
<duration-cell
v-else-if="isDuration"
v-model="localState"
:active="active"
:is-form="isForm"
:column="column"
:is-locked="isLocked"
v-on="parentListeners"
/>
<boolean-cell
v-else-if="isBoolean"
v-model="localState"
@ -65,6 +75,7 @@
v-else-if="isTime"
v-model="localState"
v-on="parentListeners"
@save="$emit('save')"
/>
<date-time-picker-cell
@ -86,7 +97,6 @@
:is-form="isForm"
:column="column"
v-on="parentListeners"
@input="$emit('save')"
/>
<json-editable-cell
@ -102,7 +112,6 @@
v-model="localState"
:column="column"
v-on="parentListeners"
@input="$emit('save')"
/>
<set-list-cell
v-else-if="isSet"
@ -148,6 +157,7 @@ import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell'
import SetListEditableCell from '~/components/project/spreadsheet/components/editableCell/SetListEditableCell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import DurationCell from '~/components/project/spreadsheet/components/editableCell/DurationCell'
export default {
name: 'EditableCell',
@ -167,7 +177,8 @@ export default {
TextAreaCell,
DateTimePickerCell,
TextCell,
DatePickerCell
DatePickerCell,
DurationCell
},
mixins: [cell],
props: {
@ -199,10 +210,10 @@ export default {
if (val !== this.value) {
this.changed = true
this.$emit('input', val)
if (this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime || this.isDate) {
this.syncData()
} else if (!this.isCurrency && !this.isEnum && !this.isSet) {
if (this.isAutoSaved) {
this.syncDataDebounce(this)
} else if (!this.isManualSaved) {
this.saveData()
}
}
}
@ -230,7 +241,7 @@ export default {
// this.$refs.input.focus();
},
beforeDestroy() {
if (this.changed && !(this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime)) {
if (this.changed && this.isAutoSaved) {
this.changed = false
this.$emit('change')
}
@ -242,6 +253,12 @@ export default {
this.changed = false
this.$emit('update')
}
},
saveData() {
if (this.changed && !this.destroyed) {
this.changed = false
this.$emit('save')
}
}
}
}

69
packages/nc-gui/components/project/spreadsheet/components/cell/DurationCell.vue

@ -0,0 +1,69 @@
<template>
<input
v-model="localValue"
:placeholder="durationPlaceholder"
readonly
>
</template>
<script>
import { durationOptions, convertMS2Duration } from '~/helpers/durationHelper'
export default {
name: 'DurationCell',
props: {
column: Object,
value: [String, Number]
},
data: () => ({
showWarningMessage: false,
localValue: null
}),
computed: {
durationPlaceholder() {
return durationOptions[this.column?.meta?.duration || 0].title
}
},
watch: {
'column.meta.duration'(newValue, oldValue) {
if (oldValue !== newValue) {
this.localValue = convertMS2Duration(this.value, newValue)
}
},
value(val, oldVal) {
this.localValue = convertMS2Duration(val !== oldVal && (!val && val !== 0) ? oldVal : val, this.column?.meta?.duration || 0)
}
},
created() {
this.localValue = convertMS2Duration(this.value, this.column?.meta?.duration || 0)
}
}
</script>
<style scoped>
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@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/>.
*
*/
-->

72
packages/nc-gui/components/project/spreadsheet/components/editColumn/DurationOptions.vue

@ -0,0 +1,72 @@
<template>
<v-row class="duration-wrapper">
<div class="caption">
A duration of time in minutes or seconds (e.g. 1:23).
</div>
<!-- TODO: i18n -->
<v-autocomplete
v-model="colMeta.duration"
hide-details
class="caption ui-type nc-ui-dt-dropdown"
label="Duration Format"
dense
outlined
item-value="id"
item-text="title"
:items="durationOptionList"
>
<template #selection="{ item }">
<div>
<span class="caption grey--text text--darken-4">
{{ item.title }}
</span>
</div>
</template>
<template #item="{ item }">
<div class="caption">
{{ item.title }}
</div>
</template>
</v-autocomplete>
</v-row>
</template>
<script>
import { durationOptions } from '~/helpers/durationHelper'
export default {
name: 'DuractionOptions',
props: ['column', 'meta', 'value'],
data: () => ({
durationOptionList: durationOptions.map(o => ({
...o,
// h:mm:ss (e.g. 3:45, 1:23:40)
title: `${o.title} ${o.example}`
})),
colMeta: {
duration: 0
}
}),
watch: {
value() {
this.colMeta = this.value || {}
},
colMeta(v) {
this.$emit('input', v)
}
},
created() {
this.colMeta = this.value ? { ...this.value } : { ...this.colMeta }
}
}
</script>
<style scoped>
.duration-wrapper {
margin: 0;
}
.duration-wrapper .caption:first-child {
margin: -10px 0px 10px 5px;
}
</style>

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') {

139
packages/nc-gui/components/project/spreadsheet/components/editableCell/DurationCell.vue

@ -0,0 +1,139 @@
<template>
<div class="duration-cell-wrapper">
<input
ref="durationInput"
v-model="localState"
:placeholder="durationPlaceholder"
@blur="onBlur"
@keypress="checkDurationFormat($event)"
@keydown.enter="isEdited && $emit('input', durationInMS)"
v-on="parentListeners"
>
<div v-if="showWarningMessage == true" class="duration-warning">
<!-- TODO: i18n -->
Please enter a number
</div>
</div>
</template>
<script>
import { durationOptions, convertMS2Duration, convertDurationToSeconds } from '~/helpers/durationHelper'
export default {
name: 'DurationCell',
props: {
column: Object,
value: [Number, String],
readOnly: Boolean
},
data: () => ({
// flag to determine to show warning message or not
showWarningMessage: false,
// duration in milliseconds
durationInMS: null,
// check if the cell is edited or not
isEdited: false
}),
computed: {
localState: {
get() {
return convertMS2Duration(this.value, this.durationType)
},
set(val) {
this.isEdited = true
const res = convertDurationToSeconds(val, this.durationType)
if (res._isValid) {
this.durationInMS = res._sec
}
}
},
durationPlaceholder() {
return durationOptions[this.durationType].title
},
durationType() {
return this.column?.meta?.duration || 0
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
mounted() {
window.addEventListener('keypress', (_) => {
if (this.$refs.durationInput) {
this.$refs.durationInput.focus()
}
})
},
methods: {
checkDurationFormat(evt) {
evt = evt || window.event
const charCode = (evt.which) ? evt.which : evt.keyCode
// ref: http://www.columbia.edu/kermit/ascii.html
const PRINTABLE_CTL_RANGE = charCode > 31
const NON_DIGIT = charCode < 48 || charCode > 57
const NON_COLON = charCode !== 58
const NON_PERIOD = charCode !== 46
if (PRINTABLE_CTL_RANGE && NON_DIGIT && NON_COLON && NON_PERIOD) {
this.showWarningMessage = true
evt.preventDefault()
} else {
this.showWarningMessage = false
// only allow digits, '.' and ':' (without quotes)
return true
}
},
onBlur() {
if (this.isEdited) {
this.$emit('input', this.durationInMS)
}
this.isEdited = false
}
}
}
</script>
<style scoped>
.duration-cell-wrapper {
padding: 10px;
}
.duration-warning {
text-align: left;
margin-top: 10px;
color: #E65100;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@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/>.
*
*/
-->

3
packages/nc-gui/components/project/spreadsheet/components/editableCell/TimePickerCell.vue

@ -5,7 +5,7 @@
</template>
<div class="d-flex flex-column justify-center" @click.stop>
<v-time-picker v-model="localState" v-on="parentListeners" />
<v-btn small color="primary" @click="$emit('update')">
<v-btn small color="primary" @click="$emit('save')">
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
@ -15,7 +15,6 @@
<script>
import dayjs from 'dayjs'
import { MysqlUi } from 'nocodb-sdk'
export default {
name: 'TimePickerCell',

29
packages/nc-gui/components/project/spreadsheet/mixins/cell.js

@ -67,8 +67,34 @@ export default {
},
isCurrency() {
return this.uiDatatype === 'Currency'
},
isDuration() {
return this.uiDatatype === UITypes.Duration
},
isAutoSaved() {
return [
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.Email,
UITypes.URL,
UITypes.Number,
UITypes.Decimal,
UITypes.Percent,
UITypes.Count,
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry
].includes(this.uiDatatype)
},
isManualSaved() {
return [
UITypes.Currency,
UITypes.Year,
UITypes.Time,
UITypes.Duration
].includes(this.uiDatatype)
}
}
}
/**
@ -76,6 +102,7 @@ export default {
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

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: {

192
packages/nc-gui/helpers/durationHelper.js

@ -0,0 +1,192 @@
export const durationOptions = [
{
id: 0,
title: 'h:mm',
example: '(e.g. 1:23)',
regex: /(\d+)(?::(\d+))?/
}, {
id: 1,
title: 'h:mm:ss',
example: '(e.g. 3:45, 1:23:40)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?/
}, {
id: 2,
title: 'h:mm:ss.s',
example: '(e.g. 3:34.6, 1:23:40.0)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}, {
id: 3,
title: 'h:mm:ss.ss',
example: '(e.g. 3.45.67, 1:23:40.00)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}, {
id: 4,
title: 'h:mm:ss.sss',
example: '(e.g. 3.45.678, 1:23:40.000)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}
]
// pad zero
// mm && ss
// e.g. 3 -> 03
// e.g. 12 -> 12
// sss
// e.g. 1 -> 001
// e.g. 10 -> 010
const padZero = (val, isSSS = false) => {
return (val + '').padStart(isSSS ? 3 : 2, '0')
}
export const convertMS2Duration = (val, durationType) => {
if (val === "" || val === null || val === undefined) { return val }
// 600.000 s --> 10:00 (10 mins)
const milliseconds = Math.round((val % 1) * 1000)
const centiseconds = Math.round(milliseconds / 10)
const deciseconds = Math.round(centiseconds / 10)
const hours = Math.floor(parseInt(val, 10) / (60 * 60))
const minutes = Math.floor((parseInt(val, 10) - (hours * 60 * 60)) / 60)
const seconds = parseInt(val, 10) - (hours * 60 * 60) - (minutes * 60)
if (durationType === 0) {
// h:mm
return `${padZero(hours)}:${padZero(minutes + (seconds >= 30))}`
} else if (durationType === 1) {
// h:mm:ss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`
} else if (durationType === 2) {
// h:mm:ss.s
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${deciseconds}`
} else if (durationType === 3) {
// h:mm:ss.ss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${padZero(centiseconds)}`
} else if (durationType === 4) {
// h:mm:ss.sss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${padZero(milliseconds, true)}`
}
return val
}
export const convertDurationToSeconds = (val, durationType) => {
// 10:00 (10 mins) -> 600.000 s
const res = {
_ms: null,
_isValid: true
}
const durationRegex = durationOptions[durationType].regex
if (durationRegex.test(val)) {
let h, mm, ss
const groups = val.match(durationRegex)
if (groups[0] && groups[1] && !groups[2] && !groups[3] && !groups[4]) {
const val = parseInt(groups[1], 10)
if (groups.input.slice(-1) === ':') {
// e.g. 30:
h = groups[1]
mm = 0
ss = 0
} else if (durationType === 0) {
// consider it as minutes
// e.g. 360 -> 06:00
h = Math.floor(val / 60)
mm = Math.floor((val - ((h * 3600)) / 60))
ss = 0
} else {
// consider it as seconds
// e.g. 3600 -> 01:00:00
h = Math.floor(groups[1] / 3600)
mm = Math.floor(groups[1] / 60) % 60
ss = val % 60
}
} else if (durationType !== 0 && groups[1] && groups[2] && !groups[3]) {
// 10:10 means mm:ss instead of h:mm
// 10:10:10 means h:mm:ss
h = 0
mm = groups[1]
ss = groups[2]
} else {
h = groups[1] || 0
mm = groups[2] || 0
ss = groups[3] || 0
}
if (durationType === 0) {
// h:mm
res._sec = h * 3600 + mm * 60
} else if (durationType === 1) {
// h:mm:ss
res._sec = h * 3600 + mm * 60 + ss * 1
} else if (durationType === 2) {
// h:mm:ss.s (deciseconds)
const ds = groups[4] || 0
const len = Math.log(ds) * Math.LOG10E + 1 | 0
const ms = (
// e.g. len = 4: 1234 -> 1, 1456 -> 1
// e.g. len = 3: 123 -> 1, 191 -> 2
// e.g. len = 2: 12 -> 1 , 16 -> 2
len === 4
? Math.round(ds / 1000)
: len === 3
? Math.round(ds / 100)
: len === 2
? Math.round(ds / 10)
// take whatever it is
: ds
) * 100
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
} else if (durationType === 3) {
// h:mm:ss.ss (centiseconds)
const cs = groups[4] || 0
const len = Math.log(cs) * Math.LOG10E + 1 | 0
const ms = (
// e.g. len = 4: 1234 -> 12, 1285 -> 13
// e.g. len = 3: 123 -> 12, 128 -> 13
// check the third digit
len === 4
? Math.round(cs / 100)
: len === 3
? Math.round(cs / 10)
// take whatever it is
: cs
) * 10
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
} else if (durationType === 4) {
// h:mm:ss.sss (milliseconds)
let ms = groups[4] || 0
const len = Math.log(ms) * Math.LOG10E + 1 | 0
ms = (
// e.g. 1235 -> 124
// e.g. 1234 -> 123
len === 4
? Math.round(ms / 10)
// take whatever it is
: ms
) * 1
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
}
} else {
res._isValid = false
}
return res
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@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/>.
*
*/

288
packages/nc-gui/lang/it_IT.json

@ -1,49 +1,49 @@
{
"general": {
"home": "Casa",
"load": "Caricare",
"open": "Aprire",
"close": "Vicino",
"yes": "sì",
"home": "Home",
"load": "Carica",
"open": "Apri",
"close": "Chiudi",
"yes": "Sì",
"no": "No",
"ok": "ok",
"ok": "OK",
"and": "e",
"or": "o",
"add": "Aggiungere",
"edit": "Modificare",
"remove": "Rimuovere",
"add": "Aggiungi",
"edit": "Modifica",
"remove": "Rimuovi",
"save": "Salva",
"cancel": "Annulla",
"submit": "Invia",
"create": "Crea",
"insert": "Inserire",
"delete": "Eliminare",
"update": "Aggiornare",
"rename": "Rinominare",
"reload": "Ricaricare",
"insert": "Inserisci",
"delete": "Elimina",
"update": "Aggiorna",
"rename": "Rinomina",
"reload": "Ricarica",
"reset": "Ripristina",
"install": "Installare",
"show": "Spettacolo",
"hide": "Nascondere",
"install": "Installa",
"show": "Mostra",
"hide": "Nascondi",
"showAll": "Mostra tutto",
"hideAll": "Nascondi tutto",
"showMore": "Mostra di più",
"showOptions": "Mostra le opzioni",
"hideOptions": "Nascondi opzioni",
"showMenu": "Mostra menu.",
"hideMenu": "Nascondere il menu",
"hideOptions": "Nascondi le opzioni",
"showMenu": "Mostra il menu",
"hideMenu": "Nascondi il menu",
"addAll": "Aggiungi tutto",
"removeAll": "Rimuovi tutto",
"signUp": "Iscriviti",
"signUp": "REGISTRATI",
"signIn": "ACCEDI",
"signOut": "Disconnessione",
"signOut": "Esci",
"required": "Richiesto",
"preferred": "Preferito",
"mandatory": "Obbligatorio",
"loading": "Caricamento ...",
"title": "Titolo",
"upload": "Caricamento",
"download": "Scaricamento",
"upload": "Carica",
"download": "Scarica",
"default": "Predefinito",
"more": "Di più",
"less": "Meno",
@ -51,7 +51,7 @@
"condition": "Condizione",
"after": "Dopo",
"before": "Prima",
"search": "Ricerca",
"search": "Cerca",
"notification": "Notifica",
"reference": "Riferimento",
"function": "Funzione"
@ -59,7 +59,7 @@
"objects": {
"project": "Progetto",
"projects": "Progetti",
"table": "Tavolo",
"table": "Tabella",
"tables": "Tabelle",
"field": "Campo",
"fields": "Campi",
@ -67,11 +67,11 @@
"columns": "Colonne",
"page": "Pagina",
"pages": "Pagine",
"record": "Disco",
"records": "Record",
"webhook": "Webhook.",
"webhooks": "Webhooks.",
"view": "Visualizzazione",
"record": "Record",
"records": "Records",
"webhook": "Webhook",
"webhooks": "Webhooks",
"view": "Vista",
"views": "Viste",
"viewType": {
"grid": "Griglia",
@ -97,14 +97,14 @@
"ForeignKey": "Chiave straniera",
"SingleLineText": "Testo a riga singola",
"LongText": "Testo lungo",
"Attachment": "Attaccamento",
"Attachment": "Allegato",
"Checkbox": "Casella di controllo",
"MultiSelect": "Selezione multipla",
"SingleSelect": "Seleziona singola",
"SingleSelect": "Selezione singola",
"Collaborator": "Collaboratore",
"Date": "Data",
"Year": "Anno",
"Time": "Volta",
"Time": "Orario",
"PhoneNumber": "Numero di telefono",
"Email": "E-mail",
"URL": "URL",
@ -115,21 +115,21 @@
"Duration": "Durata",
"Rating": "Valutazione",
"Formula": "Formula",
"Rollup": "Rollup.",
"Count": "Contare",
"Lookup": "Consultare",
"DateTime": "Appuntamento",
"CreateTime": "Creare tempo",
"LastModifiedTime": "Ultimo tempo modificato",
"AutoNumber": "Numero Auto.",
"Barcode": "Codici a barre",
"Rollup": "Rollup",
"Count": "Contatore",
"Lookup": "Consultazione",
"DateTime": "Data e ora",
"CreateTime": "Data di creazione",
"LastModifiedTime": "Data di ultima modifica",
"AutoNumber": "Numerazione automatica",
"Barcode": "Codice a barre",
"Button": "Pulsante",
"Password": "Parola d'ordine",
"Password": "Password",
"relationProperties": {
"noAction": "Nessuna azione",
"cascade": "Cascata",
"restrict": "Limitare",
"setNull": "Set null.",
"restrict": "Limitata",
"setNull": "Lascia vuoto",
"setDefault": "Imposta default"
}
},
@ -140,7 +140,7 @@
"isNot like": "non è come",
"isEmpty": "è vuoto",
"isNotEmpty": "non è vuoto",
"isNull": zero",
"isNull": nullo",
"isNotNull": "non è nullo"
},
"title": {
@ -148,10 +148,10 @@
"myProject": "I miei progetti",
"formTitle": "Titolo del modulo",
"collabView": "Vista collaborativa",
"lockedView": "Vista chiusa",
"lockedView": "Vista bloccata",
"personalView": "Vista personale",
"appStore": "App Store",
"teamAndAuth": "Team & Autorizzazioni",
"teamAndAuth": "Team e Autorizzazioni",
"rolesUserMgmt": "Gestione dei ruoli e degli utenti",
"userMgmt": "Gestione degli utenti",
"apiTokenMgmt": "Gestione dei token API",
@ -159,60 +159,60 @@
"projMeta": "Metadati del progetto",
"metaMgmt": "Gestione metadati",
"metadata": "Metadati",
"exportImportMeta": "Esportazione / importazione metadati",
"uiACL": "Controllo degli accessi ui.",
"exportImportMeta": "Esporta / Importa metadati",
"uiACL": "Controllo degli accessi interfaccia grafica (ACL)",
"metaOperations": "Operazioni sui metadati",
"audit": "Audit.",
"auditLogs": "Registro di audizione",
"sqlMigrations": "Migrazioni SQL.",
"audit": "Audit",
"auditLogs": "Registro audit",
"sqlMigrations": "Migrazioni SQL",
"dbCredentials": "Credenziali del database",
"advancedParameters": "SSL e parametri avanzati",
"headCreateProject": "Crea progetto | NocoDB",
"headLogin": "Accedi | NocoDB",
"resetPassword": "Reimposta la tua password",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
"teamAndSettings": "Team e Impostazioni",
"apiDocs": "Documentazione API",
"importFromAirtable": "Importa da Airtable"
},
"labels": {
"notifyVia": "Notifica via.",
"notifyVia": "Notifica tramite",
"projName": "Nome del progetto",
"tableName": "Nome della tabella",
"viewName": "Visualizza nome.",
"viewLink": "Visualizza link.",
"viewName": "Visualizza nome",
"viewLink": "Visualizza link",
"columnName": "Nome della colonna",
"columnType": "Tipo di colonna",
"roleName": "Nome del ruolo",
"roleDescription": "Descrizione del ruolo",
"databaseType": "Digita il database",
"lengthValue": "Lunghezza / valore",
"databaseType": "Digita nel database",
"lengthValue": "Lunghezza / Valore",
"dbType": "Tipo di database",
"sqliteFile": "File di SQLite",
"hostAddress": "Indirizzo host",
"hostAddress": "Indirizzo dell'host",
"port": "Numero di porta",
"username": "Nome utente",
"password": "Password",
"schemaName": "Schema name",
"schemaName": "Nome schema",
"action": "Azione",
"actions": "Azioni",
"operation": "Operazione",
"operationType": "Tipo di funzionamento",
"operationSubType": "Operazione Sub-Type",
"operationType": "Tipo di operazioni",
"operationSubType": "Sottotipo di operazioni",
"description": "Descrizione",
"authentication": "Autenticazione",
"token": "Gettone",
"token": "Chiave (Token)",
"where": "Dove",
"cache": "Cache.",
"chat": "Chiacchierata",
"cache": "Cache",
"chat": "Chat",
"email": "E-mail",
"storage": "Conservazione",
"storage": "Archiviazione",
"uiAcl": "UI-ACL",
"models": "Modelli",
"syncState": "Sincronizza State.",
"syncState": "Sincronizza stato",
"created": "Creato",
"sqlOutput": "Uscita SQL.",
"addOption": "Aggiungi opzione.",
"aggregateFunction": "Funzione aggregata",
"sqlOutput": "Output SQL",
"addOption": "Aggiungi opzione",
"aggregateFunction": "Aggrega funzione",
"dbCreateIfNotExists": "Database: crea se non esiste",
"clientKey": "Chiave client",
"clientCert": "Certificato client",
@ -224,25 +224,25 @@
"columnName": "Inflessione - Nome della colonna"
},
"community": {
"starUs1": "Dacci una stella",
"starUs1": "Mettici una stella",
"starUs2": "su Github",
"bookDemo": "Prenota una demo gratuita",
"getAnswered": "Ottieni risposte alle tue domande",
"joinDiscord": "Unisciti su Discord",
"joinCommunity": "Join NocoDB Community",
"joinReddit": "Iscriviti /r/NocoDB",
"joinCommunity": "Unisciti alla community di NocoDB",
"joinReddit": "Unisciti a /r/NocoDB su Reddit",
"followNocodb": "Segui NocoDB"
},
"docReference": "Riferimento del documento",
"selectUserRole": "Seleziona il ruolo utente",
"childTable": "Tavolo da bambino",
"childColumn": "Colonna del bambino",
"onUpdate": "Su Aggiorna.",
"onDelete": "Su Elimina"
"childTable": "Sottotabella",
"childColumn": "Sottocolonna",
"onUpdate": "All'aggiornamento",
"onDelete": "All'eliminazione"
},
"activity": {
"createProject": "Crea progetto",
"importProject": "Progetto di importazione",
"importProject": "Importa progetto",
"searchProject": "Cerca progetto",
"editProject": "Modifica progetto",
"stopProject": "Interrompi progetto",
@ -254,7 +254,7 @@
"createProjectExtended": {
"extDB": "Crea collegandoti <br>ad un database esterno",
"excel": "Crea un progetto da Excel",
"template": "Crea progetto dal modello"
"template": "Crea progetto da un modello"
},
"OkSaveProject": "OK & Salva progetto",
"upgrade": {
@ -279,14 +279,14 @@
"enable": "Chiunque con il link",
"link": "Collegamento base condiviso"
},
"invite": "Invitare",
"invite": "Invita",
"inviteMore": "Invita di più",
"inviteTeam": "Invita il team.",
"inviteToken": "Invita il token.",
"inviteTeam": "Invita un team.",
"inviteToken": "Invita con una chiave",
"newUser": "Nuovo utente",
"editUser": "Modifica utente",
"deleteUser": "Rimuovi utente dal progetto",
"resendInvite": "Inserisci inviti e-mail",
"resendInvite": "Reinvia e-mail di invito",
"copyInviteURL": "Copia l'URL dell'invito",
"newRole": "Nuovo ruolo",
"reloadRoles": "Ricarica ruoli",
@ -295,10 +295,10 @@
"nextRecord": "Prossimo record.",
"previousRecord": "Record precedente",
"copyApiURL": "Copia l'URL delle API",
"createTable": "Creare la tabella.",
"refreshTable": "Tavoli aggiornati",
"renameTable": "Tabella Rinomina.",
"deleteTable": "Tabella Elimina.",
"createTable": "Crea tabella.",
"refreshTable": "Aggiorna tabella",
"renameTable": "Rinomina tabella",
"deleteTable": "Elimina tabella",
"addField": "Aggiungi un nuovo campo a questa tabella",
"setPrimary": "Impostare come valore primario",
"addRow": "Aggiungi nuova riga",
@ -308,14 +308,14 @@
"deleteSelectedRow": "Elimina righe selezionate",
"importExcel": "Importa Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Scarica come csv.",
"uploadCSV": "Carica csv.",
"downloadCSV": "Scarica come CSV.",
"uploadCSV": "Carica CSV.",
"import": "Importa",
"importMetadata": "Importa metadati",
"exportMetadata": "Esporta metadati",
"clearMetadata": "Cancella metadati",
"exportToFile": "Esporta su file",
"changePwd": "Cambiare la password",
"changePwd": "Cambia password",
"createView": "Crea una vista",
"shareView": "Condividi vista",
"listSharedView": "Elenco di visualizzazione condivisa",
@ -323,15 +323,15 @@
"copyView": "Copia vista",
"renameView": "Rinomina vista",
"deleteView": "Elimina vista",
"createGrid": "Creare Grid View.",
"createGallery": "Crea vista Galleria",
"createCalendar": "Crea vista del calendario",
"createGrid": "Crea vista griglia",
"createGallery": "Crea vista galleria",
"createCalendar": "Crea vista calendario",
"createKanban": "Crea vista Kanban.",
"createForm": "Crea vista modulo",
"showSystemFields": "Mostra campi di sistema",
"copyUrl": "Copia URL.",
"openTab": "Apri la nuova scheda",
"iFrame": "Copia codice HTML incorporato",
"openTab": "Apri una nuova scheda",
"iFrame": "Copia codice HTML incorporabile",
"addWebhook": "Aggiungi nuovo webhook.",
"newToken": "Aggiungi nuovo token.",
"exportZip": "Esporta zip",
@ -340,36 +340,36 @@
"settings": "Impostazioni",
"previewAs": "Anteprima come",
"resetReview": "Ripristina anteprima",
"testDbConn": "Testa connessione al database",
"testDbConn": "Verifica connessione al database",
"removeDbFromEnv": "Rimuovi il database dall'ambiente",
"editConnJson": "Modifica JSON della connessione",
"sponsorUs": "Sponsorizzaci",
"sendEmail": "INVIA EMAIL"
},
"tooltip": {
"saveChanges": "Salvare le modifiche",
"saveChanges": "Salva le modifiche",
"xcDB": "Crea un nuovo progetto",
"extDB": "Supporta MySQL, PostgreSQL, SQL Server & SQLite",
"apiRest": "Accessibile tramite API REST",
"apiGQL": "Accessibile tramite API GraphQL",
"theme": {
"dark": "Viene in nero (^ ⇧b)",
"light": "Viene in nero? (^ ⇧b)"
"dark": "C'è la modalità scura (^ ⇧b)",
"light": "C'è la modalità scura? (^ ⇧b)"
},
"addTable": "Aggiungi nuova tabella",
"inviteMore": "Invita più utenti",
"toggleNavDraw": "Disattivazione del cassetto di navigazione",
"reloadApiToken": "Ricarica i token API.",
"generateNewApiToken": "Genera nuovo token API",
"toggleNavDraw": "Attiva / Disattiva il pannello di navigazione",
"reloadApiToken": "Ricarica le chiavi API.",
"generateNewApiToken": "Genera nuova chiave API",
"addRole": "Aggiungi un nuovo ruolo",
"reloadList": "Elenco di ricarica",
"metaSync": "Sincronizzazione metadati.",
"reloadList": "Ricarica lista",
"metaSync": "Sincronizza metadati",
"sqlMigration": "Ricarica le migrazioni",
"updateRestart": "Aggiorna e riavvia",
"cancelReturn": "Annulla e torna indietro",
"exportMetadata": "Esporta tutti i metadati dalle tabelle meta alla cartella meta.",
"importMetadata": "Importa tutti i metadati dalla cartella meta alle tabelle meta.",
"clearMetadata": "Cancella tutti i metadati dalle tabelle meta.",
"exportMetadata": "Esporta tutti i metadati dalla sopratabella alla sopracartella",
"importMetadata": "Importa tutti i metadati dalla sopracartella alla sopratabella",
"clearMetadata": "Cancella tutti i metadati dalle sopratabelle",
"clientKey": "Seleziona file .key",
"clientCert": "Seleziona file .cert",
"clientCA": "Seleziona file CA"
@ -386,9 +386,9 @@
"searchProjectTree": "Cerca tabelle",
"searchFields": "Campi di ricerca",
"searchColumn": "Cerca {Cerca} colonna",
"searchApps": "Cerca apps.",
"searchApps": "Cerca app",
"searchModels": "Modelli di ricerca",
"noItemsFound": "Nessun articolo trovato",
"noItemsFound": "Nessun elemento trovato",
"defaultValue": "Valore di default",
"filterByEmail": "Filtra per e-mail"
},
@ -396,49 +396,49 @@
"info": {
"footerInfo": "Righe per pagina",
"upload": "Seleziona il file da caricare",
"upload_sub": "o trascinare e rilasciare il file",
"excelSupport": "Supportato: .xls, .xlsx, .xlsm, .ods, .ots",
"upload_sub": "o trascinalo qui",
"excelSupport": "Tipi supportati: .xls, .xlsx, .xlsm, .ods, .ots",
"excelURL": "Inserisci l'URL del file Excel",
"csvURL": "Enter CSV file URL",
"footMsg": "# di righe per analizzare per dedurre il tipo di dati",
"footMsg": "Numero di righe per analizzare per dedurre il tipo di dati",
"excelImport": "foglio (i) sono disponibili per l'importazione",
"exportMetadata": "Vuoi esportare i metadati da Meta Tables?",
"importMetadata": "Vuoi importare i metadati da Meta Tables?",
"clearMetadata": "Vuoi cancellare i metadati da Meta Tables?",
"exportMetadata": "Vuoi esportare i metadati dalle sopracartelle?",
"importMetadata": "Vuoi importare i metadati dalle sopracartelle?",
"clearMetadata": "Vuoi cancellare i metadati dalle sopracartelle?",
"projectEmptyMessage": "Inizia creando un nuovo progetto",
"stopProject": "Vuoi fermare il progetto?",
"startProject": "Vuoi iniziare il progetto?",
"startProject": "Vuoi avviare il progetto?",
"restartProject": "Vuoi riavviare il progetto?",
"deleteProject": "Vuoi cancellare il progetto?",
"shareBasePrivate": "Genera base readonly condivisibile pubblicamente condivisa",
"shareBasePrivate": "Genera base readonly condivisibile pubblicamente",
"shareBasePublic": "Chiunque su Internet con questo link può visualizzare",
"userInviteNoSMTP": "Sembra che non abbia ancora configurato mailer!\nSi prega di copiare sopra Invita link e invialo a",
"dragDropHide": "Trascina e rilascia i campi qui per nascondersi",
"userInviteNoSMTP": "Sembra che non abbia ancora configurato il mailer! \\n Manda il link d'invito sopra a",
"dragDropHide": "Rilascia qui i campi da nascondere",
"formInput": "Inserisci l'etichetta di input del modulo",
"formHelpText": "Aggiungi un po 'di testo di aiuto",
"formHelpText": "Aggiungi testo di aiuto",
"onlyCreator": "Solo visibile al creatore",
"formDesc": "Aggiungi modulo Descrizione",
"beforeEnablePwd": "Limitare l'accesso con una password",
"formDesc": "Aggiungi descrizione del modulo",
"beforeEnablePwd": "Limita l'accesso con una password",
"afterEnablePwd": "L'accesso è protetto da una password",
"privateLink": "Questa vista è condivisa tramite un collegamento privato",
"privateLinkAdditionalInfo": "Le persone con collegamento privato possono vedere solo le celle visibili in questa vista",
"afterFormSubmitted": "Dopo la forma è presentato",
"afterFormSubmitted": "Dopo che il modulo è stato inviato",
"apiOptions": "Accedi al progetto tramite",
"submitAnotherForm": "Mostra \"Invia un altro modulo\"",
"showBlankForm": "Mostra un modulo vuoto dopo 5 secondi",
"emailForm": "E-mail me a",
"emailForm": "Mandami un'e-mail a",
"showSysFields": "Mostra campi di sistema",
"filterAutoApply": "Auto Apply.",
"showMessage": "Mostra questo messaggio",
"viewNotShared": "La vista corrente non è condivisa!",
"showAllViews": "Mostra tutte le viste condivise di questa tabella",
"collabView": "I collaboratori con autorizzazioni di modifica o superiore possono modificare la configurazione di visualizzazione.",
"lockedView": "Nessuno può modificare la configurazione di visualizzazione finché non è sbloccata.",
"personalView": "Solo è possibile modificare la configurazione di visualizzazione. Le viste personali di altri collaboratori sono nascoste per impostazione predefinita.",
"ownerDesc": "Può aggiungere / rimuovere i creatori. E strutture di database di modifica completa e campi.",
"collabView": "I collaboratori con autorizzazioni di modifica o superiore possono modificare la configurazione della vista",
"lockedView": "Nessuno può modificare la configurazione della vista finché non è sbloccata.",
"personalView": "Solo tu puoi modificare la visualizzazione della vista. Le viste personali degli altri collaboratori sono nascoste di default",
"ownerDesc": "Può aggiungere / rimuovere i creatori. E modificare completamente strutture e campi",
"creatorDesc": "Può modificare completamente la struttura e i valori del database.",
"editorDesc": "Può modificare i record ma non può cambiare la struttura del database / campi.",
"commenterDesc": "Può visualizzare e commentare i record ma non è possibile modificare nulla",
"editorDesc": "Può modificare i record ma non può cambiare la struttura del database / dei campi.",
"commenterDesc": "Può visualizzare e commentare i record ma non può modificare nulla",
"viewerDesc": "Può visualizzare i record ma non può modificare nulla",
"addUser": "Aggiungi nuovo utente",
"staticRoleInfo": "I ruoli definiti dal sistema non possono essere modificati",
@ -455,7 +455,7 @@
},
"sponsor": {
"header": "Puoi aiutarci!",
"message": "Siamo un piccolo team che lavora a tempo pieno per rendere l'open-source di NOCODB. Crediamo che uno strumento come NOCODB dovrebbe essere disponibile liberamente per ogni risolutore di problemi su Internet."
"message": "Siamo un piccolo team che lavora a tempo pieno per rendere NocoDB open-source. Crediamo che uno strumento come NocoDB dovrebbe essere disponibile liberamente per problem-solver su Internet."
},
"loginMsg": "Accedi a NocoDB",
"passwordRecovery": {
@ -478,8 +478,8 @@
"kanban": "Aggiungi vista kanban",
"calendar": "Aggiungi vista calendario"
},
"tablesMetadataInSync": "Le tabelle i metadati sono sincronizzati",
"addMultipleUsers": "Puoi aggiungere più e-mail di virgola (,) separate",
"tablesMetadataInSync": "I metadati delle tabelle sono sincronizzati",
"addMultipleUsers": "Puoi aggiungere più e-mail separate da virgole(,)",
"enterTableName": "Inserisci il nome della tabella",
"addDefaultColumns": "Aggiungi colonne predefinite",
"tableNameInDb": "Nome della tabella come salvato nel database"
@ -488,8 +488,8 @@
"searchProject": "La tua ricerca di {search} non ha trovato risultati",
"invalidChar": "Carattere non valido nel percorso della cartella.",
"invalidDbCredentials": "Credenziali del database non valide.",
"unableToConnectToDb": "Impossibile connettersi al database, controllare che il database sia attivo.",
"userDoesntHaveSufficientPermission": "L'utente non esiste o non dispone di autorizzazioni sufficienti per creare schema.",
"unableToConnectToDb": "Impossibile connettersi al database, controllare che il database sia avviato.",
"userDoesntHaveSufficientPermission": "L'utente non esiste o non dispone di autorizzazioni sufficienti per creare uno schema.",
"dbConnectionStatus": "Parametri del database non validi",
"dbConnectionFailed": "Connessione fallita:",
"signUpRules": {
@ -502,15 +502,15 @@
"toast": {
"exportMetadata": "Metadati del progetto esportati con successo",
"importMetadata": "Metadati del progetto importati con successo",
"clearMetadata": "I metadati del progetto hanno cancellato con successo",
"stopProject": "Il progetto si è fermato con successo",
"startProject": "Il progetto è iniziato con successo",
"clearMetadata": "Metadati del progetto cancellati con successo",
"stopProject": "Progetto fermato con successo",
"startProject": "Progetto avviato con successo",
"restartProject": "Progetto riavviato con successo",
"deleteProject": "Progetto cancellato con successo",
"authToken": "Autunque Token copiava negli Appunti",
"authToken": "Chiave di autenticazione copiata negli Appunti",
"projInfo": "Informazioni del progetto copiate negli Appunti",
"inviteUrlCopy": "Copiato Invita URL negli Appunti",
"createView": "Visualizza creato con successo",
"inviteUrlCopy": "Copiato URL di invito negli Appunti",
"createView": "Vista creata con successo",
"formEmailSMTP": "Si prega di attivare il plugin SMTP in App Store per abilitare la notifica e-mail",
"collabView": "Passato correttamente alla vista collaborativa",
"lockedView": "Passato correttamente alla vista bloccata",

44
packages/nc-gui/package-lock.json generated

@ -28,7 +28,7 @@
"monaco-editor": "^0.19.3",
"monaco-themes": "^0.2.5",
"nano-assign": "^1.0.1",
"nocodb-sdk": "0.91.10",
"nocodb-sdk": "file:../nocodb-sdk",
"nuxt": "^2.14.0",
"odometer": "^0.4.8",
"papaparse": "^5.3.1",
@ -76,7 +76,6 @@
},
"../nocodb-sdk": {
"version": "0.91.10",
"extraneous": true,
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
@ -10899,16 +10898,8 @@
}
},
"node_modules/nocodb-sdk": {
"version": "0.91.10",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"engines": {
"node": ">=10"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-fetch": {
"version": "2.6.7",
@ -26326,12 +26317,33 @@
}
},
"nocodb-sdk": {
"version": "0.91.10",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"version": "file:../nocodb-sdk",
"requires": {
"@ava/typescript": "^1.1.1",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^3.12.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"gh-pages": "^3.1.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^6.0.1",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.0",
"typescript": "^4.0.2"
}
},
"node-fetch": {

2
packages/nc-gui/package.json

@ -31,7 +31,7 @@
"monaco-editor": "^0.19.3",
"monaco-themes": "^0.2.5",
"nano-assign": "^1.0.1",
"nocodb-sdk": "0.91.10",
"nocodb-sdk": "file:../nocodb-sdk",
"nuxt": "^2.14.0",
"odometer": "^0.4.8",
"papaparse": "^5.3.1",

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() {

98
packages/nc-gui/pages/user/authentication/passwordValidateMixin.js

@ -1,3 +1,5 @@
import { validatePassword } from 'nocodb-sdk'
export default {
data: () => ({
passwordProgress: 0,
@ -20,54 +22,62 @@ export default {
return this.formUtil.progressColorValue
},
PasswordValidate(p) {
if (!p) {
this.passwordProgress = 0
this.passwordValidateMsg = 'At least 8 letters with one Uppercase, one number and one special letter'
return false
}
let msg = ''
let validation = true
let progress = 0
if (!(p.length >= 8)) {
msg += 'Atleast 8 letters. '
validation = validation && false
} else {
progress = Math.min(100, progress + 25)
}
if (!(p.match(/.*[A-Z].*/))) {
msg += 'One Uppercase Letter. '
validation = validation && false
} else {
progress = Math.min(100, progress + 25)
}
if (!(p.match(/.*[0-9].*/))) {
msg += 'One Number. '
validation = validation && false
} else {
progress = Math.min(100, progress + 25)
}
if (!(p.match(/[$&+,:;=?@#|'<>.^*()%!_-]/))) {
msg += 'One special letter. '
validation = validation && false
} else {
progress = Math.min(100, progress + 25)
}
const { error, progress, valid } = validatePassword(p)
if (valid) { return true }
this.formUtil.passwordProgress = progress
// console.log('progress', progress);
// console.log('color', this.progressColor(this.formUtil.passwordProgress));
this.progressColorValue = this.progressColor(this.formUtil.passwordProgress)
this.formUtil.passwordValidateMsg = msg
// console.log('msg', msg, validation);
return validation
this.formUtil.passwordValidateMsg = error
return error
// if (!p) {
// this.passwordProgress = 0
// this.passwordValidateMsg = 'At least 8 letters with one Uppercase, one number and one special letter'
// return false
// }
//
// let msg = ''
// let validation = true
// let progress = 0
//
// if (!(p.length >= 8)) {
// msg += 'Atleast 8 letters. '
// validation = validation && false
// } else {
// progress = Math.min(100, progress + 25)
// }
//
// if (!(p.match(/.*[A-Z].*/))) {
// msg += 'One Uppercase Letter. '
// validation = validation && false
// } else {
// progress = Math.min(100, progress + 25)
// }
//
// if (!(p.match(/.*[0-9].*/))) {
// msg += 'One Number. '
// validation = validation && false
// } else {
// progress = Math.min(100, progress + 25)
// }
//
// if (!(p.match(/[$&+,:;=?@#|'<>.^*()%!_-]/))) {
// msg += 'One special letter. '
// validation = validation && false
// } else {
// progress = Math.min(100, progress + 25)
// }
//
// this.formUtil.passwordProgress = progress
// // console.log('progress', progress);
// // console.log('color', this.progressColor(this.formUtil.passwordProgress));
// this.progressColorValue = this.progressColor(this.formUtil.passwordProgress)
//
// this.formUtil.passwordValidateMsg = msg
//
// // console.log('msg', msg, validation);
//
// return validation
}
}

5
packages/nc-gui/pages/user/authentication/signin.vue

@ -244,10 +244,7 @@ export default {
],
password: [
// Password is required
v => !!v || this.$t('msg.error.signUpRules.passwdRequired'),
// You password must be atleast 8 characters
v =>
(v && v.length >= 8) || this.$t('msg.error.signUpRules.passwdLength')
v => !!v || this.$t('msg.error.signUpRules.passwdRequired')
]
},
formUtil: {

16
packages/nc-plugin/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nc-plugin",
"version": "0.1.1",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nc-plugin",
"version": "0.1.1",
"version": "0.1.3",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^1.17.1",
@ -10298,9 +10298,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"node_modules/shelljs": {
@ -19881,9 +19881,9 @@
"dev": true
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"shelljs": {

14
packages/noco-blog/package-lock.json generated

@ -5836,16 +5836,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -10797,9 +10797,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

28
packages/noco-docs-prev/package-lock.json generated

@ -7390,16 +7390,16 @@
}
},
"node_modules/got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -13804,9 +13804,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
@ -22650,16 +22650,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -27611,9 +27611,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

24
packages/noco-docs/content/en/developer-resources/rest-apis.md

@ -21,15 +21,15 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Category | Method | Tag | Function Name | Path |
|---|---|---|---|---|
| Auth | Post | auth | signup | /api/v1/db/auth/user/signup |
| Auth | Post | auth | signin | /api/v1/db/auth/user/signin |
| Auth | Get | auth | me | /api/v1/db/auth/user/me |
| Auth | Post | auth | passwordForgot | /api/v1/db/auth/password/forgot |
| Auth | Post | auth | passwordChange | /api/v1/db/auth/password/change |
| Auth | Post | auth | passwordReset | /api/v1/db/auth/password/reset/{token} |
| Auth | Post | auth | tokenRefresh | /api/v1/db/auth/token/refresh |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/db/auth/token/validate/{token} |
| Auth | Post | auth | emailValidate | /api/v1/db/auth/email/validate/{email} |
| Auth | Post | auth | signup | /api/v1/auth/user/signup |
| Auth | Post | auth | signin | /api/v1/auth/user/signin |
| Auth | Get | auth | me | /api/v1/auth/user/me |
| Auth | Post | auth | passwordForgot | /api/v1/auth/password/forgot |
| Auth | Post | auth | passwordChange | /api/v1/auth/password/change |
| Auth | Post | auth | passwordReset | /api/v1/auth/password/reset/{token} |
| Auth | Post | auth | tokenRefresh | /api/v1/auth/token/refresh |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/auth/token/validate/{token} |
| Auth | Post | auth | emailValidate | /api/v1/auth/email/validate/{email} |
### Public APIs
@ -166,7 +166,8 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Meta | Delete| utils | cacheDelete | /api/v1/db/meta/cache |
| Meta | Post | utils | testConnection | /api/v1/db/meta/projects/connection/test |
| Meta | Get | utils | appInfo | /api/v1/db/meta/nocodb/info |
| Meta | Get | utils | appVersion | /api/v1/db/meta/nocodb/version |
| Meta | Get | utils | appVersion | /api/v1/version |
| Meta | Get | utils | appHealth | /api/v1/health |
## Query params
@ -193,7 +194,8 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Operation | Meaning | Example |
|---|---|---|
| eq | equal | (colName,eq,colValue) |
| not | not equal | (colName,not,colValue) |
| neq | not equal | (colName,neq,colValue) |
| not | not equal (alias of neq) | (colName,not,colValue) |
| gt | greater than | (colName,gt,colValue) |
| ge | greater or equal | (colName,ge,colValue) |
| lt | less than | (colName,lt,colValue) |

42
packages/noco-docs/content/en/engineering/timely-build.md

@ -0,0 +1,42 @@
---
title: "Timely Build"
description: "Timely Build"
position: 5000
category: "Engineering"
menuTitle: "Timely Build"
---
NocoDB provides timely build versions on Docker and Executables by compiling our source code and packaging as a deliverable so that it can
- reduce pull request cycle time
- allow issue reporters / reviewers to verify the fix without setting up their local machines
## Docker
When a non-draft Pull Request is created, reopened or synchronized, a timely build for Docker would be triggered for the changes only included in the following paths.
- `packages/nocodb-sdk/**`
- `packages/nc-gui/**`
- `packages/nc-plugin/**`
- `packages/nocodb/**`
The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely/tags) for the full list). Once the image is ready, Github bot will add a comment with the command in the pull request. The tag would be `<NOCODB_CURRENT_VERSION>-pr-<PR_NUMBER>-<YYYYMMDD>-<HHMM>`.
![image](https://user-images.githubusercontent.com/35857179/175012097-240dab05-da93-4c4e-87c1-1c36fb1350bd.png)
## Executables
Similarly, we provide a timely build for executables for non-docker users. The source code will be built, packaged as binary files, and pushed to Github (See [nocodb/nocodb-timely](https://github.com/nocodb/nocodb-timely/releases) for the full list).
Currently, we only support the following targets:
- `node16-linux-arm64`
- `node16-macos-arm64`
- `node16-win-arm64`
- `node16-linux-x64`
- `node16-macos-x64`
- `node16-win-x64`
Once the executables are ready, Github bot will add a comment with the commands in the pull request.
![image](https://user-images.githubusercontent.com/35857179/175012070-f5f3e7b8-6dc5-4d1c-9f7e-654bc5039521.png)

21
packages/noco-docs/content/en/getting-started/installation.md

@ -57,6 +57,14 @@ npm install
npm start
```
### Homebrew
```bash
brew tap nocodb/nocodb
brew install nocodb
nocodb
```
### Docker
If you are a Docker user, you may try this way!
@ -206,6 +214,19 @@ It is mandatory to configure `NC_DB` environment variables for production usecas
| AWS_SECRET_ACCESS_KEY | No | For Litestream - S3 secret access key | If Litestream is configured and NC_DB is not present. SQLite gets backed up to S3 | |
| AWS_BUCKET | No | For Litestream - S3 bucket | If Litestream is configured and NC_DB is not present. SQLite gets backed up to S3 | |
| AWS_BUCKET_PATH | No | For Litestream - S3 bucket path (like folder within S3 bucket) | If Litestream is configured and NC_DB is not present. SQLite gets backed up to S3 | |
| NC_SMTP_FROM | No | For SMTP plugin - Email sender address | | |
| NC_SMTP_HOST | No | For SMTP plugin - SMTP host value | | |
| NC_SMTP_PORT | No | For SMTP plugin - SMTP port value | | |
| NC_SMTP_USERNAME | No | For SMTP plugin (Optional) - SMTP username value for authentication | | |
| NC_SMTP_PASSWORD | No | For SMTP plugin (Optional) - SMTP password value for authentication | | |
| NC_SMTP_SECURE | No | For SMTP plugin (Optional) - To enable secure set value as `true` any other value treated as false | | |
| NC_SMTP_IGNORE_TLS | No | For SMTP plugin (Optional) - To ignore tls set value as `true` any other value treated as false. For more info visit https://nodemailer.com/smtp/ | | |
| NC_S3_BUCKET_NAME | No | For S3 storage plugin - AWS S3 bucket name | | |
| NC_S3_REGION | No | For S3 storage plugin - AWS S3 region | | |
| NC_S3_ACCESS_KEY | No | For S3 storage plugin - AWS access key credential for accessing resource | | |
| NC_S3_ACCESS_SECRET | No | For S3 storage plugin - AWS access secret credential for accessing resource | | |
| NC_ADMIN_EMAIL | No | For updating/creating super admin with provided email and password | | |
| NC_ADMIN_PASSWORD | No | For updating/creating super admin with provided email and password. Your password should have at least 8 letters with one uppercase, one number and one special letter(Allowed special chars <code>$&+,:;=?@#&#124;'.^*()%!_-"</code> ) | | |
### AWS ECS (Fargate)

2
packages/noco-docs/content/en/setup-and-usages/dashboard.md

@ -16,7 +16,7 @@ Click `Let's Begin` button to sign up.
Enter your work email and your password.
<alert>
<alert id="password-conditions">
Your password has at least 8 letters with one uppercase, one number and one special letter
</alert>

44
packages/noco-docs/content/en/setup-and-usages/primary-key.md

@ -0,0 +1,44 @@
---
title: "Primary Key"
description: "Primary Key"
position: 575
category: "Product"
menuTitle: "Primary Key"
---
## What is a Primary Key ?
- A primary key is a special database table column designated to uniquely identify each table record.
## What is the use of Primary Key ?
- As it uniquely identifies an individual record of a table, it is used internally by NocoDB for all operations associated with a record
## Primary Key in NocoDB
- Primary Key that gets defined / used in NocoDB depends on how underlying table was created. Summary is captured below
1. From UI, Create new table / Import from Excel / Import from CSV
1. An `ID` [datatype: Integer] system field created by default during table creation is used as primary key
2. Additional system fields `created-at`, `updated-at` are inserted by default & can be omitted optionally; these fields can be deleted after table creation
2. Connect to existing external database
1. Existing `primary key` field defined for a table is retained as is; NocoDB doesn't insert a new ID field
2. Additional system fields `created-at`, `updated-at` are not inserted by default
3. Import from Airtable
1. Airtable record ID is marked as primary key for imported records, and is mapped to field `ncRecordId` [datatype: varchar]
2. If a new record is inserted after migration & if ncRecordId field was omitted during record insertion - auto generated string will be inserted by NocoDB
3. Computed hash value for the entire record is stored in system field `ncRecordHash`
4. Additional system fields `created-at`, `updated-at` are not inserted by default
4. Create new table using SDK / API
1. No default primary key field is introduced by NocoDB. It has to be explicitly specified during table creation (using attribute `pk: true`)
## What if Primary Key was missing?
It is possible to have a table without any primary key.
- External database table can be created without primary key configuration.
- New table can be created using SDK / API without primary key
In such scenario's, new records can be created in NocoDB for this table, but records can't be updated or deleted [as there is now way for NocoDB to uniquely identify these records]
#### Example : Primary Key & optional system fields during new table creation
![Screenshot 2022-06-16 at 12 15 43 PM](https://user-images.githubusercontent.com/86527202/174010350-8610b9c1-a761-4bff-a53d-dc728df47e1b.png)
#### Example : Show System Fields
![Screenshot 2022-06-16 at 12 16 07 PM](https://user-images.githubusercontent.com/86527202/174010379-9e300d42-ad89-4653-afa2-f70fca407ca8.png)
## Can I change the Primary Key to another column within tables ?
- You can't update Primary Key from NocoDB UI. You can reconfigure it at database level directly & trigger `metasync` explicitly

2
packages/noco-docs/content/en/setup-and-usages/primary-value.md

@ -1,7 +1,7 @@
---
title: "Primary value"
description: "Primary value"
position: 575
position: 580
category: "Product"
menuTitle: "Primary value"
---

28
packages/noco-docs/package-lock.json generated

@ -7390,16 +7390,16 @@
}
},
"node_modules/got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -13804,9 +13804,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
@ -22650,16 +22650,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -27611,9 +27611,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

6
packages/noco-i18n/package-lock.json generated

@ -9793,9 +9793,9 @@
"dev": true
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"side-channel": {

2
packages/nocodb-sdk/src/index.ts

@ -7,3 +7,5 @@ export * from './lib/helperFunctions';
export * from './lib/formulaHelpers';
export { default as UITypes, isVirtualCol } from './lib/UITypes';
export { default as CustomAPI } from './lib/CustomAPI';
export { default as TemplateGenerator } from './lib/TemplateGenerator';
export * from './lib/passwordHelpers';

56
packages/nocodb-sdk/src/lib/Api.ts

@ -805,7 +805,7 @@ export class Api<
* @tags Auth
* @name Signup
* @summary Signup
* @request POST:/api/v1/db/auth/user/signup
* @request POST:/api/v1/auth/user/signup
* @response `200` `{ token?: string }` OK
* @response `400` `{ msg?: string }` Bad Request
* @response `401` `void` Unauthorized
@ -816,7 +816,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<{ token?: string }, { msg?: string } | void>({
path: `/api/v1/db/auth/user/signup`,
path: `/api/v1/auth/user/signup`,
method: 'POST',
body: data,
format: 'json',
@ -829,7 +829,7 @@ export class Api<
* @tags Auth
* @name Signin
* @summary Signin
* @request POST:/api/v1/db/auth/user/signin
* @request POST:/api/v1/auth/user/signin
* @response `200` `{ token?: string }` OK
* @response `400` `{ msg?: string }` Bad Request
*/
@ -838,7 +838,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<{ token?: string }, { msg?: string }>({
path: `/api/v1/db/auth/user/signin`,
path: `/api/v1/auth/user/signin`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -852,12 +852,12 @@ export class Api<
* @tags Auth
* @name Me
* @summary User info
* @request GET:/api/v1/db/auth/user/me
* @request GET:/api/v1/auth/user/me
* @response `200` `UserInfoType` OK
*/
me: (query?: { project_id?: string }, params: RequestParams = {}) =>
this.request<UserInfoType, any>({
path: `/api/v1/db/auth/user/me`,
path: `/api/v1/auth/user/me`,
method: 'GET',
query: query,
format: 'json',
@ -870,13 +870,13 @@ export class Api<
* @tags Auth
* @name PasswordForgot
* @summary Password forgot
* @request POST:/api/v1/db/auth/password/forgot
* @request POST:/api/v1/auth/password/forgot
* @response `200` `void` OK
* @response `401` `void` Unauthorized
*/
passwordForgot: (data: { email?: string }, params: RequestParams = {}) =>
this.request<void, void>({
path: `/api/v1/db/auth/password/forgot`,
path: `/api/v1/auth/password/forgot`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -889,7 +889,7 @@ export class Api<
* @tags Auth
* @name PasswordChange
* @summary Password change
* @request POST:/api/v1/db/auth/password/change
* @request POST:/api/v1/auth/password/change
* @response `200` `{ msg?: string }` OK
* @response `400` `{ msg?: string }` Bad request
*/
@ -898,7 +898,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<{ msg?: string }, { msg?: string }>({
path: `/api/v1/db/auth/password/change`,
path: `/api/v1/auth/password/change`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -912,12 +912,12 @@ export class Api<
* @tags Auth
* @name PasswordResetTokenValidate
* @summary Reset token verify
* @request POST:/api/v1/db/auth/token/validate/{token}
* @request POST:/api/v1/auth/token/validate/{token}
* @response `200` `void` OK
*/
passwordResetTokenValidate: (token: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/auth/token/validate/${token}`,
path: `/api/v1/auth/token/validate/${token}`,
method: 'POST',
...params,
}),
@ -928,12 +928,12 @@ export class Api<
* @tags Auth
* @name EmailValidate
* @summary Verify email
* @request POST:/api/v1/db/auth/email/validate/{token}
* @request POST:/api/v1/auth/email/validate/{token}
* @response `200` `void` OK
*/
emailValidate: (token: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/auth/email/validate/${token}`,
path: `/api/v1/auth/email/validate/${token}`,
method: 'POST',
...params,
}),
@ -944,7 +944,7 @@ export class Api<
* @tags Auth
* @name PasswordReset
* @summary Password reset
* @request POST:/api/v1/db/auth/password/reset/{token}
* @request POST:/api/v1/auth/password/reset/{token}
* @response `200` `void` OK
*/
passwordReset: (
@ -953,7 +953,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<void, any>({
path: `/api/v1/db/auth/password/reset/${token}`,
path: `/api/v1/auth/password/reset/${token}`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -966,12 +966,12 @@ export class Api<
* @tags Auth
* @name TokenRefresh
* @summary Refresh token
* @request POST:/api/v1/db/auth/token/refresh
* @request POST:/api/v1/auth/token/refresh
* @response `200` `void` OK
*/
tokenRefresh: (params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/auth/token/refresh`,
path: `/api/v1/auth/token/refresh`,
method: 'POST',
...params,
}),
@ -3197,12 +3197,28 @@ export class Api<
*
* @tags Utils
* @name AppVersion
* @request GET:/api/v1/db/meta/nocodb/version
* @request GET:/api/v1/version
* @response `200` `any` OK
*/
appVersion: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/db/meta/nocodb/version`,
path: `/api/v1/version`,
method: 'GET',
format: 'json',
...params,
}),
/**
* No description
*
* @tags Utils
* @name AppHealth
* @request GET:/api/v1/health
* @response `200` `any` OK
*/
appHealth: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/health`,
method: 'GET',
format: 'json',
...params,

30
packages/nocodb-sdk/src/lib/TemplateGenerator.ts

@ -0,0 +1,30 @@
import UITypes from './UITypes';
export interface Column {
column_name: string;
ref_column_name: string;
uidt?: UITypes;
dtxp?: any;
dt?: any;
}
export interface Table {
table_name: string;
ref_table_name: string;
columns: Array<Column>;
}
export interface Template {
title: string;
tables: Array<Table>;
}
export default abstract class TemplateGenerator {
abstract parse(): Promise<any>;
abstract parseTemplate(): Promise<Template>;
abstract getColumns(): Promise<any>;
abstract parseData(): Promise<any>;
abstract getData(): Promise<{
[table_name: string]: Array<{
[key: string]: any;
}>;
}>;
}

41
packages/nocodb-sdk/src/lib/passwordHelpers.ts

@ -0,0 +1,41 @@
export function validatePassword(p) {
let error = '';
let progress = 0;
let hint = null;
let valid = true;
if (!p) {
error =
'At least 8 letters with one Uppercase, one number and one special letter';
valid = false;
} else {
if (!(p.length >= 8)) {
error += 'Atleast 8 letters. ';
valid = false;
} else {
progress = Math.min(100, progress + 25);
}
if (!p.match(/.*[A-Z].*/)) {
error += 'One Uppercase Letter. ';
valid = false;
} else {
progress = Math.min(100, progress + 25);
}
if (!p.match(/.*[0-9].*/)) {
error += 'One Number. ';
valid = false;
} else {
progress = Math.min(100, progress + 25);
}
if (!p.match(/[$&+,:;=?@#|'<>.^*()%!_-]/)) {
error += 'One special letter. ';
hint = "Allowed special character list : $&+,:;=?@#|'<>.^*()%!_-";
valid = false;
} else {
progress = Math.min(100, progress + 25);
}
}
return { error, valid, progress, hint };
}

7
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 {
@ -1144,7 +1147,7 @@ export class MssqlUi {
colProp.dt = 'double';
break;
case 'Duration':
colProp.dt = 'int';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'int';

2
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -1036,7 +1036,7 @@ export class MysqlUi {
colProp.dt = 'double';
break;
case 'Duration':
colProp.dt = 'int';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'int';

4
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -1660,7 +1660,7 @@ export class PgUi {
colProp.dt = 'double precision';
break;
case 'Duration':
colProp.dt = 'int8';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'smallint';
@ -1740,7 +1740,7 @@ export class PgUi {
return ['json', 'char', 'character', 'character varying', 'text'];
case 'JSON':
return ['json', 'text'];
return ['json', 'jsonb', 'text'];
case 'Checkbox':
return [
'bit',

2
packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts

@ -853,7 +853,7 @@ export class SqliteUi {
colProp.dt = 'double';
break;
case 'Duration':
colProp.dt = 'integer';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'integer';

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

58
packages/nocodb/package-lock.json generated

@ -70,11 +70,11 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.61",
"nc-help": "0.2.67",
"nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "0.91.10",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"ora": "^4.0.4",
@ -232,7 +232,6 @@
},
"../nocodb-sdk": {
"version": "0.91.10",
"extraneous": true,
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
@ -16353,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.67",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.67.tgz",
"integrity": "sha512-O9eXHrpO0dBdFv6zUZAos+63JZEGhZ2lG+MduGZ+/BL7M5b0qU7d9b95Pmgq6Gd5wO3txT/7x7uPBHZxeSgvHQ==",
"dependencies": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",
@ -16571,16 +16570,8 @@
"dev": true
},
"node_modules/nocodb-sdk": {
"version": "0.91.10",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"engines": {
"node": ">=10"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-addon-api": {
"version": "2.0.0",
@ -38049,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.67",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.67.tgz",
"integrity": "sha512-O9eXHrpO0dBdFv6zUZAos+63JZEGhZ2lG+MduGZ+/BL7M5b0qU7d9b95Pmgq6Gd5wO3txT/7x7uPBHZxeSgvHQ==",
"requires": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",
@ -38231,12 +38222,33 @@
"dev": true
},
"nocodb-sdk": {
"version": "0.91.10",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"version": "file:../nocodb-sdk",
"requires": {
"@ava/typescript": "^1.1.1",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^3.12.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"gh-pages": "^3.1.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^6.0.1",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.0",
"typescript": "^4.0.2"
}
},
"node-addon-api": {

5
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,11 +154,11 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.61",
"nc-help": "0.2.67",
"nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "0.91.10",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"ora": "^4.0.4",

10
packages/nocodb/src/__tests__/restv2.test.ts

@ -209,7 +209,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Rollup,
alias: 'filmCount',
rollupColumn: 'FilmId',
relationColumn: 'FilmMMList',
relationColumn: 'Film List',
rollupFunction: 'count'
}
];
@ -413,7 +413,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'filmNames',
lookupColumn: 'Title',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
};
request(app)
.post(`/nc/${projectId}/generate`)
@ -1335,7 +1335,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'filmIds',
lookupColumn: 'FilmId',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
},
{
table: 'actor',
@ -1398,7 +1398,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Rollup,
alias: 'actorsCount',
rollupColumn: 'ActorId',
relationColumn: 'ActorMMList',
relationColumn: 'ActorList',
rollupFunction: 'count'
},
{
@ -1406,7 +1406,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'actorsCountList',
lookupColumn: 'actorsCount',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
},
{
table: 'actor',

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

@ -42,6 +42,7 @@ import { Tele } from 'nc-help';
import * as http from 'http';
import weAreHiring from './utils/weAreHiring';
import getInstance from './utils/getInstance';
import initAdminFromEnv from './meta/api/userApi/initAdminFromEnv';
const log = debug('nc:app');
require('dotenv').config();
@ -186,8 +187,8 @@ export default class Noco {
}
await Noco._ncMeta.metaInit();
await this.readOrGenJwtSecret();
await this.initJwt();
await initAdminFromEnv();
await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta });
@ -488,8 +489,9 @@ export default class Noco {
}
}
private async readOrGenJwtSecret(): Promise<any> {
if (this.config?.auth?.jwt && !this.config.auth.jwt.secret) {
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'
@ -503,6 +505,13 @@ export default class Noco {
}
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';
}
}
let serverId = (
await Noco._ncMeta.metaGet('', '', 'nc_store', {
key: 'nc_server_id'

86
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -41,6 +41,7 @@ import { customValidators } from './customValidators';
import { NcError } from '../../../../meta/helpers/catchError';
import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { sanitize, unsanitize } from './helpers/sanitize';
const GROUP_COL = '__nc_group_id';
@ -90,7 +91,7 @@ class BaseModelSqlv2 {
}
public async readByPk(id?: any): Promise<any> {
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
@ -106,7 +107,7 @@ class BaseModelSqlv2 {
}
public async exist(id?: any): Promise<any> {
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
const pks = this.model.primaryKeys;
if ((id + '').split('___').length != pks.length) {
@ -119,12 +120,14 @@ class BaseModelSqlv2 {
args: {
where?: string;
filterArr?: Filter[];
sort?: string | string[];
} = {}
): Promise<any> {
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
const aliasColObjMap = await this.model.getAliasColObjMap();
const sorts = extractSortsObject(args?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
await conditionV2(
@ -145,6 +148,12 @@ class BaseModelSqlv2 {
this.dbDriver
);
if (Array.isArray(sorts) && sorts?.length) {
await sortV2(sorts, qb, this.dbDriver);
} else if (this.model.primaryKey) {
qb.orderBy(this.model.primaryKey.column_name);
}
const data = await qb.first();
if (data) {
@ -167,15 +176,12 @@ class BaseModelSqlv2 {
): Promise<any> {
const { where, ...rest } = this._getListArgs(args as any);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
const aliasColObjMap = await this.model.getAliasColObjMap();
let sorts = extractSortsObject(args?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
// todo: replace with view id
if (!ignoreFilterSort && this.viewId) {
await conditionV2(
@ -241,7 +247,6 @@ class BaseModelSqlv2 {
if (!ignoreFilterSort) applyPaginate(qb, rest);
const proto = await this.getProto();
const data = await this.extractRawQueryAndExec(qb);
return data?.map(d => {
@ -257,7 +262,7 @@ class BaseModelSqlv2 {
await this.model.getColumns();
const { where } = this._getListArgs(args);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
// qb.xwhere(where, await this.model.getAliasColMapping());
const aliasColObjMap = await this.model.getAliasColObjMap();
@ -306,11 +311,11 @@ class BaseModelSqlv2 {
);
}
qb.count(this.model.primaryKey?.column_name || '*', {
qb.count(sanitize(this.model.primaryKey?.column_name) || '*', {
as: 'count'
}).first();
return ((await qb) as any).count;
const res = (await this.dbDriver.raw(unsanitize(qb.toQuery()))) as any;
return (this.isPg ? res.rows[0] : res[0][0] ?? res[0]).count;
}
async groupBy(
@ -326,7 +331,7 @@ class BaseModelSqlv2 {
) {
const { where, ...rest } = this._getListArgs(args as any);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
qb.count(`${this.model.primaryKey?.column_name || '*'} as count`);
qb.select(args.column_name);
@ -907,7 +912,7 @@ class BaseModelSqlv2 {
const proto = await childModel.getProto();
return (await qb).map(c => {
return (await this.extractRawQueryAndExec(qb)).map(c => {
c.__proto__ = proto;
return c;
});
@ -993,8 +998,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, args);
const proto = await parentModel.getProto();
return (await qb).map(c => {
return (await this.extractRawQueryAndExec(qb)).map(c => {
c.__proto__ = proto;
return c;
});
@ -1242,7 +1246,7 @@ class BaseModelSqlv2 {
await populatePk(this.model, data);
// todo: filter based on view
const insertObj = await this.model.mapAliasToColumn(data, sanitize);
const insertObj = await this.model.mapAliasToColumn(data);
await this.validate(insertObj);
@ -1258,12 +1262,11 @@ class BaseModelSqlv2 {
// const driver = trx ? trx : this.dbDriver;
const query = this.dbDriver(this.tnPath).insert(insertObj);
if (this.isPg || this.isMssql) {
query.returning(
`${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}`
);
response = await query;
response = await this.extractRawQueryAndExec(query);
}
const ai = this.model.columns.find(c => c.ai);
@ -1275,11 +1278,19 @@ class BaseModelSqlv2 {
if (response?.length) {
id = response[0];
} else {
id = (await query)[0];
const res = await this.extractRawQueryAndExec(query);
id = res?.id ?? res[0]?.insertId;
}
if (ai) {
// response = await this.readByPk(id)
if (this.isSqlite) {
// sqlite doesnt return id after insert
id = (
await this.dbDriver(this.tnPath)
.select(ai.column_name)
.max(ai.column_name, { as: 'id' })
)[0].id;
}
response = await this.readByPk(id);
} else {
response = data;
@ -1326,14 +1337,11 @@ class BaseModelSqlv2 {
await this.beforeUpdate(data, trx, cookie);
// const driver = trx ? trx : this.dbDriver;
//
// this.validate(data);
// await this._run(
await this.dbDriver(this.tnPath)
const query = this.dbDriver(this.tnPath)
.update(updateObj)
.where(await this._wherePk(id));
// );
await this.extractRawQueryAndExec(query);
const response = await this.readByPk(id);
await this.afterUpdate(response, trx, cookie);
@ -1610,7 +1618,7 @@ class BaseModelSqlv2 {
} else {
await this.model.getColumns();
const { where } = this._getListArgs(args);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
const aliasColObjMap = await this.model.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
@ -1672,7 +1680,7 @@ class BaseModelSqlv2 {
try {
await this.model.getColumns();
const { where } = this._getListArgs(args);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
const aliasColObjMap = await this.model.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
@ -2029,11 +2037,19 @@ class BaseModelSqlv2 {
}
private async extractRawQueryAndExec(qb: QueryBuilder) {
let query = qb.toQuery();
if (!this.isPg && !this.isMssql) {
query = unsanitize(qb.toQuery());
} else {
query = sanitize(query);
}
return this.isPg
? qb
: await this.dbDriver.from(
this.dbDriver.raw(qb.toString()).wrap('(', ') __nc_alias')
);
? (await this.dbDriver.raw(query))?.rows
: query.slice(0, 6) === 'select'
? await this.dbDriver.from(
this.dbDriver.raw(query).wrap('(', ') __nc_alias')
)
: await this.dbDriver.raw(query);
}
}
@ -2168,10 +2184,6 @@ function getCompositePk(primaryKeys: Column[], row) {
return primaryKeys.map(c => row[c.title]).join('___');
}
export function sanitize(v) {
return v?.replace(/([^\\]|^)([?])/g, '$1\\$2');
}
export { BaseModelSqlv2 };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

12
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts

@ -10,6 +10,7 @@ import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import FormulaColumn from '../../../../models/FormulaColumn';
import { RelationTypes, UITypes } from 'nocodb-sdk';
// import LookupColumn from '../../../models/LookupColumn';
import { sanitize } from './helpers/sanitize';
export default async function conditionV2(
conditionObj: Filter | Filter[],
@ -203,11 +204,13 @@ const parseConditionV2 = async (
filter.comparison_op === 'notempty'
)
filter.value = '';
let field = customWhereClause
let field = sanitize(
customWhereClause
? filter.value
: alias
? `${alias}.${column.column_name}`
: column.column_name;
: column.column_name
);
let val = customWhereClause ? customWhereClause : filter.value;
return qb => {
@ -216,12 +219,13 @@ const parseConditionV2 = async (
qb = qb.where(field, val);
break;
case 'neq':
case 'not':
qb = qb.whereNot(field, val);
break;
case 'like':
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%')
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
} else {
val = `%${val}%`;
}
@ -234,7 +238,7 @@ const parseConditionV2 = async (
case 'nlike':
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%')
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
} else {
val = `%${val}%`;
}

2
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts

@ -86,7 +86,7 @@ const getAst = async ({
(!fields?.length || fields.includes(col.title)) &&
value
: fields?.length
? fields.includes(col.title)
? fields.includes(col.title) && value
: value
};
}, Promise.resolve({}));

9
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/sanitize.ts

@ -0,0 +1,9 @@
export function sanitize(v) {
return v?.replace(/([^\\]|^)(\?+)/g, (_, m1, m2) => {
return `${m1}${m2.split('?').join('\\?')}`;
});
}
export function unsanitize(v) {
return v?.replace(/\\[?]/g, '?');
}

3
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/sortV2.ts

@ -8,6 +8,7 @@ import LookupColumn from '../../../../models/LookupColumn';
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import FormulaColumn from '../../../../models/FormulaColumn';
import { RelationTypes, UITypes } from 'nocodb-sdk';
import { sanitize } from './helpers/sanitize';
export default async function sortV2(
sortList: Sort[],
@ -205,7 +206,7 @@ export default async function sortV2(
}
break;
default:
qb.orderBy(`${column.column_name}`, sort.direction || 'asc');
qb.orderBy(sanitize(column.column_name), sort.direction || 'asc');
break;
}
}

5
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaPg.ts

@ -439,7 +439,7 @@ class ModelXcMetaPg extends BaseModelXcMeta {
case 'set':
return 'MultiSelect';
case 'json':
return 'LongText';
return 'JSON';
case 'blob':
return 'LongText';
case 'geometry':
@ -512,9 +512,8 @@ class ModelXcMetaPg extends BaseModelXcMeta {
case 'interval':
return 'string';
case 'json':
return 'json';
case 'jsonb':
return 'string';
return 'json';
case 'language_handler':
case 'lsec':

4
packages/nocodb/src/lib/jobs/RedisJobsMgr.ts

@ -11,7 +11,9 @@ export default class RedisJobsMgr extends JobsMgr {
super();
this.queue = {};
this.workers = {};
this.connection = new Redis(config);
this.connection = new Redis(config, {
maxRetriesPerRequest: null
});
}
async add(

12
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -57,7 +57,7 @@ async function createHmAndBtColumn(
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
type === 'bt' ? alias : `${parent.title}Read`
type === 'bt' ? alias : `${parent.title}`
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
@ -427,7 +427,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({
title: getUniqueColumnAliasName(
await child.getColumns(),
`${child.title}MMList`
`${parent.title} List`
),
uidt: UITypes.LinkToAnotherRecord,
type: 'mm',
@ -447,7 +447,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({
title: getUniqueColumnAliasName(
await parent.getColumns(),
req.body.title ?? `${parent.title}MMList`
req.body.title ?? `${child.title} List`
),
uidt: UITypes.LinkToAnotherRecord,
@ -503,6 +503,12 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
default:
{
colBody = getColumnPropsFromUIDT(colBody, base);
if (colBody.uidt === UITypes.Duration) {
colBody.dtxp = '20';
// by default, colBody.dtxs is 2
// Duration column needs more that that
colBody.dtxs = '4';
}
const tableUpdateBody = {
...table,
tn: table.table_name,

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);

6
packages/nocodb/src/lib/meta/api/metaDiffApis.ts

@ -671,7 +671,7 @@ export async function metaDiffSync(req, res) {
if (change.relationType === RelationTypes.BELONGS_TO) {
const title = getUniqueColumnAliasName(
childModel.columns,
`${parentModel.title || parentModel.table_name}Read`
`${parentModel.title || parentModel.table_name}`
);
await Column.insert<LinkToAnotherRecordColumn>({
uidt: UITypes.LinkToAnotherRecord,
@ -785,7 +785,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelA.columns,
`${modelB.title}MMList`
`${modelB.title} List`
),
fk_model_id: modelA.id,
fk_related_model_id: modelB.id,
@ -803,7 +803,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelB.columns,
`${modelA.title}MMList`
`${modelA.title} List`
),
fk_model_id: modelB.id,
fk_related_model_id: modelA.id,

13
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');
}
@ -226,7 +230,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}Read`
title: `${bt.rtitle}`
};
})
];
@ -412,6 +416,13 @@ export async function projectCost(req, res) {
cost = Math.min(120 * userCount, 36000);
}
Tele.event({
event: 'a:project:cost',
data: {
cost
}
});
res.json({ cost });
}

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

21
packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts

@ -166,6 +166,27 @@ export const getModelPaths = async (ctx: {
}
}
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/find-one`]: {
get: {
summary: `${ctx.tableName} find-one`,
operationId: 'db-table-row-find-one',
description: `Find first record matching the conditions.`,
tags: [ctx.tableName],
parameters: [fieldsParam, whereParam, sortParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`
}
}
}
}
}
}
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/groupby`]: {
get: {
summary: `${ctx.tableName} groupby`,

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');
}

236
packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts

@ -0,0 +1,236 @@
import User from '../../../models/User';
import { v4 as uuidv4 } from 'uuid';
import { promisify } from 'util';
import { Tele } from 'nc-help';
import bcrypt from 'bcryptjs';
import Noco from '../../../Noco';
import { CacheScope, MetaTable } from '../../../utils/globals';
import ProjectUser from '../../../models/ProjectUser';
import { validatePassword } from 'nocodb-sdk';
import boxen from 'boxen';
import NocoCache from '../../../cache/NocoCache';
const { isEmail } = require('validator');
const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 };
export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
if (process.env.NC_ADMIN_EMAIL && process.env.NC_ADMIN_PASSWORD) {
if (!isEmail(process.env.NC_ADMIN_EMAIL?.trim())) {
console.log(
'\n',
boxen(
`Provided admin email '${process.env.NC_ADMIN_EMAIL}' is not valid`,
{
title: 'Invalid admin email',
padding: 1,
borderStyle: 'double',
titleAlignment: 'center',
borderColor: 'red'
}
),
'\n'
);
process.exit(1);
}
const { valid, error, hint } = validatePassword(
process.env.NC_ADMIN_PASSWORD
);
if (!valid) {
console.log(
'\n',
boxen(`${error}${hint ? `\n\n${hint}` : ''}`, {
title: 'Invalid admin password',
padding: 1,
borderStyle: 'double',
titleAlignment: 'center',
borderColor: 'red'
}),
'\n'
);
process.exit(1);
}
let ncMeta;
try {
ncMeta = await _ncMeta.startTransaction();
const email = process.env.NC_ADMIN_EMAIL.toLowerCase().trim();
const salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
salt
);
const email_verification_token = uuidv4();
// if super admin not present
if (await User.isFirst(ncMeta)) {
const roles = 'user,super';
// roles = 'owner,creator,editor'
Tele.emit('evt', {
evt_type: 'project:invite',
count: 1
});
await User.insert(
{
firstname: '',
lastname: '',
email,
salt,
password,
email_verification_token,
roles
},
ncMeta
);
} else {
const salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
salt
);
const email_verification_token = uuidv4();
const superUser = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
roles: 'user,super'
});
if (email !== superUser.email) {
// update admin email and password and migrate projects
// if user already present and associated with some project
// check user account already present with the new admin email
const existingUserWithNewEmail = await User.getByEmail(email, ncMeta);
if (existingUserWithNewEmail?.id) {
// get all project access belongs to the existing account
// and migrate to the admin account
const existingUserProjects = await ncMeta.metaList2(
null,
null,
MetaTable.PROJECT_USERS,
{
condition: { fk_user_id: existingUserWithNewEmail.id }
}
);
for (const existingUserProject of existingUserProjects) {
const userProject = await ProjectUser.get(
existingUserProject.project_id,
superUser.id,
ncMeta
);
// if admin user already have access to the project
// then update role based on the highest access level
if (userProject) {
if (
rolesLevel[userProject.roles] >
rolesLevel[existingUserProject.roles]
) {
await ProjectUser.update(
userProject.project_id,
superUser.id,
existingUserProject.roles,
ncMeta
);
}
} else {
// if super doesn't have access then add the access
await ProjectUser.insert(
{
...existingUserProject,
fk_user_id: superUser.id
},
ncMeta
);
}
// delete the old project access entry from DB
await ProjectUser.delete(
existingUserProject.project_id,
existingUserProject.fk_user_id,
ncMeta
);
}
// delete existing user
await ncMeta.metaDelete(
null,
null,
MetaTable.USERS,
existingUserWithNewEmail.id
);
// clear cache
await NocoCache.delAll(
CacheScope.USER,
`${existingUserWithNewEmail.email}___*`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.id}`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.email}`
);
// Update email and password of super admin account
await User.update(
superUser.id,
{
salt,
email,
password,
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
} else {
// if email's are not different update the password and hash
await User.update(
superUser.id,
{
salt,
email,
password,
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
}
} else {
const newPasswordHash = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
superUser.salt
);
if (newPasswordHash !== superUser.password) {
// if email's are same and passwords are different
// then update the password and token version
await User.update(
superUser.id,
{
salt,
password,
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
}
}
}
await ncMeta.commit();
} catch (e) {
console.log('Error occurred while updating/creating admin user');
console.log(e);
await ncMeta.rollback(e);
}
}
}

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);
}

2
packages/nocodb/src/lib/meta/api/userApi/ui/auth/emailVerify.ts

@ -54,7 +54,7 @@ export default `<!DOCTYPE html>
methods: {},
async created() {
try {
const valid = (await axios.post('<%- baseUrl %>/api/v1/db/auth/email/validate/' + this.token)).data;
const valid = (await axios.post('<%- baseUrl %>/api/v1/auth/email/validate/' + this.token)).data;
this.valid = !!valid;
} catch (e) {
this.valid = false;

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

@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import { TableType } from 'nocodb-sdk';
import { TableType, validatePassword } from 'nocodb-sdk';
import catchError, { NcError } from '../../helpers/catchError';
const { isEmail } = require('validator');
import * as ejs from 'ejs';
@ -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 {
@ -31,6 +31,12 @@ export async function signup(req: Request, res: Response<TableType>) {
} = req.body;
let { password } = req.body;
// validate password and throw error if password is satisfying the conditions
const { valid, error } = validatePassword(password);
if (!valid) {
NcError.badRequest(`Password : ${error}`);
}
if (!isEmail(_email)) {
NcError.badRequest(`Invalid email`);
}
@ -173,15 +179,14 @@ async function successfulSignIn({
await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString();
let token_version = user.token_version;
if (!token_version) {
token_version = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version
token_version: user.token_version
});
setTokenCookie(res, refreshToken);
@ -237,10 +242,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 = {
@ -262,6 +263,13 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
if (!currentPassword || !newPassword) {
return NcError.badRequest('Missing new/old password');
}
// validate password and throw error if password is satisfying the conditions
const { valid, error } = validatePassword(newPassword);
if (!valid) {
NcError.badRequest(`Password : ${error}`);
}
const user = await User.getByEmail((req as any).user.email);
const hashedPassword = await promisify(bcrypt.hash)(
currentPassword,
@ -318,10 +326,10 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
subject: 'Password Reset Link',
text: `Visit following link to update your password : ${
(req as any).ncSiteUrl
}/api/v1/db/auth/password/reset/${token}.`,
}/api/v1/auth/password/reset/${token}.`,
html: ejs.render(template, {
resetLink:
(req as any).ncSiteUrl + `/api/v1/db/auth/password/reset/${token}`
(req as any).ncSiteUrl + `/api/v1/auth/password/reset/${token}`
})
})
);
@ -381,6 +389,12 @@ async function passwordReset(req, res): Promise<any> {
NcError.badRequest('Email registered via social account');
}
// validate password and throw error if password is satisfying the conditions
const { valid, error } = validatePassword(req.body.password);
if (!valid) {
NcError.badRequest(`Password : ${error}`);
}
const salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(req.body.password, salt);
@ -501,7 +515,7 @@ const mapRoutes = router => {
})(req, res, next)
);
// new API
// deprecated APIs
router.post('/api/v1/db/auth/user/signup', catchError(signup));
router.post('/api/v1/db/auth/user/signin', catchError(signin));
router.get(
@ -534,5 +548,39 @@ const mapRoutes = router => {
'/api/v1/db/auth/password/reset/:tokenId',
catchError(renderPasswordReset)
);
// new API
router.post('/api/v1/auth/user/signup', catchError(signup));
router.post('/api/v1/auth/user/signin', catchError(signin));
router.get(
'/api/v1/auth/user/me',
extractProjectIdAndAuthenticate,
catchError(me)
);
router.post('/api/v1/auth/password/forgot', catchError(passwordForgot));
router.post(
'/api/v1/auth/token/validate/:tokenId',
catchError(tokenValidate)
);
router.post(
'/api/v1/auth/password/reset/:tokenId',
catchError(passwordReset)
);
router.post(
'/api/v1/auth/email/validate/:tokenId',
catchError(emailVerification)
);
router.post(
'/api/v1/auth/password/change',
ncMetaAclMw(passwordChange, 'passwordChange')
);
router.post(
'/api/v1/auth/token/refresh',
ncMetaAclMw(refreshToken, 'refreshToken')
);
router.get(
'/api/v1/auth/password/reset/:tokenId',
catchError(renderPasswordReset)
);
};
export { mapRoutes as userApis };

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

@ -60,8 +60,17 @@ export async function releaseVersion(_req: Request, res: Response) {
res.json(result);
}
export async function axiosRequestMake(req: Request, res: Response) {
export async function appHealth(_: Request, res: Response) {
res.json({
message: 'OK',
timestamp: Date.now(),
uptime: process.uptime()
});
}
async function _axiosRequestMake(req: Request, res: Response) {
const { apiMeta } = req.body;
if (apiMeta?.body) {
try {
apiMeta.body = JSON.parse(apiMeta.body);
@ -106,12 +115,32 @@ 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',
ncMetaAclMw(testConnection, 'testConnection')
);
router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo));
router.get('/api/v1/db/meta/nocodb/version', catchError(releaseVersion));
router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake));
router.get('/api/v1/version', catchError(releaseVersion));
router.get('/api/v1/health', catchError(appHealth));
};

66
packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts

@ -31,6 +31,7 @@ import Noco from '../../Noco';
import Local from '../../v1-legacy/plugins/adapters/storage/Local';
import { MetaTable } from '../../utils/globals';
import { PluginCategory } from 'nocodb-sdk';
import Plugin from '../../models/Plugin';
const defaultPlugins = [
SlackPluginConfig,
@ -97,25 +98,54 @@ class NcPluginMgrv2 {
pluginConfig.id
);
}
}
await this.initPluginsFromEnv();
}
/* init only the active plugins */
// if (pluginConfig?.active) {
// const tempPlugin = new plugin.builder(this.app, plugin);
//
// this.activePlugins.push(tempPlugin);
//
// if (pluginConfig?.input) {
// pluginConfig.input = JSON.parse(pluginConfig.input);
// }
//
// try {
// await tempPlugin.init(pluginConfig?.input);
// } catch (e) {
// console.log(
// `Plugin(${plugin?.title}) initialization failed : ${e.message}`
// );
// }
// }
private static async initPluginsFromEnv() {
/*
* NC_S3_BUCKET_NAME
* NC_S3_REGION
* NC_S3_ACCESS_KEY
* NC_S3_ACCESS_SECRET
* */
if (
process.env.NC_S3_BUCKET_NAME &&
process.env.NC_S3_REGION &&
process.env.NC_S3_ACCESS_KEY &&
process.env.NC_S3_ACCESS_SECRET
) {
const s3Plugin = await Plugin.getPluginByTitle(S3PluginConfig.title);
await Plugin.update(s3Plugin.id, {
active: true,
input: JSON.stringify({
bucket: process.env.NC_S3_BUCKET_NAME,
region: process.env.NC_S3_REGION,
access_key: process.env.NC_S3_ACCESS_KEY,
access_secret: process.env.NC_S3_ACCESS_SECRET
})
});
}
if (
process.env.NC_SMTP_FROM &&
process.env.NC_SMTP_HOST &&
process.env.NC_SMTP_PORT
) {
const smtpPlugin = await Plugin.getPluginByTitle(SMTPPluginConfig.title);
await Plugin.update(smtpPlugin.id, {
active: true,
input: JSON.stringify({
from: process.env.NC_SMTP_FROM,
host: process.env.NC_SMTP_HOST,
port: process.env.NC_SMTP_PORT,
username: process.env.NC_SMTP_USERNAME,
password: process.env.NC_SMTP_PASSWORD,
secure: process.env.NC_SMTP_SECURE,
ignoreTLS: process.env.NC_SMTP_IGNORE_TLS
})
});
}
}

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');
}

1
packages/nocodb/src/lib/models/Filter.ts

@ -23,6 +23,7 @@ export default class Filter {
comparison_op?:
| 'eq'
| 'neq'
| 'not'
| 'like'
| 'nlike'
| 'empty'

10
packages/nocodb/src/lib/models/Model.ts

@ -20,6 +20,7 @@ import {
import View from './View';
import { NcError } from '../meta/helpers/catchError';
import Audit from './Audit';
import { sanitize } from '../db/sql-data-mapper/lib/sql/helpers/sanitize';
export default class Model implements TableType {
copy_enabled: boolean;
@ -399,13 +400,14 @@ export default class Model implements TableType {
return true;
}
async mapAliasToColumn(data, sanitize = v => v) {
async mapAliasToColumn(data) {
const insertObj = {};
for (const col of await this.getColumns()) {
if (isVirtualCol(col)) continue;
const val =
data?.[sanitize(col.column_name)] ?? data?.[sanitize(col.title)];
if (val !== undefined) insertObj[sanitize(col.column_name)] = val;
const val = data?.[col.column_name] ?? data?.[col.title];
if (val !== undefined) {
insertObj[sanitize(col.column_name)] = val;
}
}
return insertObj;
}

4
packages/nocodb/src/lib/models/Plugin.ts

@ -91,7 +91,7 @@ export default class Plugin implements PluginType {
/**
* get plugin by title
*/
public static async getPluginByTitle(title: string) {
public static async getPluginByTitle(title: string, ncMeta = Noco.ncMeta) {
let plugin =
title &&
(await NocoCache.get(
@ -99,7 +99,7 @@ export default class Plugin implements PluginType {
CacheGetType.TYPE_OBJECT
));
if (!plugin) {
plugin = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
plugin = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
title
});
await NocoCache.set(`${CacheScope.PLUGIN}:${title}`, plugin);

3
packages/nocodb/src/lib/models/User.ts

@ -84,6 +84,9 @@ export default class User implements UserType {
if (updateObj.email) {
updateObj.email = updateObj.email.toLowerCase();
} else {
// set email prop to avoid generation of invalid cache key
updateObj.email = (await this.get(id, ncMeta))?.email?.toLowerCase();
}
// get existing cache
const keys = [

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.

4
scripts/cypress/integration/common/1a_table_operations.js

@ -80,9 +80,9 @@ export const genTest = (apiType, dbType) => {
// 4a. Address table, has many field
cy.openTableTab("Address", 25);
mainPage.getCell("CityRead", 1).scrollIntoView();
mainPage.getCell("City", 1).scrollIntoView();
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".name")
.contains("Lethbridge")
.should("exist");

4
scripts/cypress/integration/common/2b_table_with_m2m_column.js

@ -23,10 +23,10 @@ export const genTest = (apiType, dbType) => {
it("Expand m2m column", () => {
// expand first row
cy.get('td[data-col="FilmMMList"] div', { timeout: 12000 })
cy.get('td[data-col="Film List"] div', { timeout: 12000 })
.first()
.click({ force: true });
cy.get('td[data-col="FilmMMList"] div .mdi-arrow-expand')
cy.get('td[data-col="Film List"] div .mdi-arrow-expand')
.first()
.click({ force: true });

275
scripts/cypress/integration/common/3e_duration_column.js

@ -0,0 +1,275 @@
import { mainPage } from "../../support/page_objects/mainPage";
import {
isTestSuiteActive,
} from "../../support/page_objects/projectConstants";
export const genTest = (apiType, dbType) => {
if (!isTestSuiteActive(apiType, dbType)) return;
describe(`${apiType.toUpperCase()} api - DURATION`, () => {
const tableName = "DurationTable";
// to retrieve few v-input nodes from their label
//
const fetchParentFromLabel = (label) => {
cy.get("label").contains(label).parents(".v-input").click();
};
// Run once before test- create table
//
before(() => {
mainPage.tabReset();
cy.createTable(tableName);
});
after(() => {
cy.deleteTable(tableName);
});
// Routine to create a new look up column
//
const addDurationColumn = (columnName, durationFormat) => {
// (+) icon at end of column header (to add a new column)
// opens up a pop up window
//
cy.get(".new-column-header").click();
// Column name
cy.get(".nc-column-name-input input").clear().type(`${columnName}`);
// Column data type
cy.get(".nc-ui-dt-dropdown").click();
cy.getActiveMenu().contains("Duration").click();
// Configure Child table & column names
fetchParentFromLabel("Duration Format");
cy.getActiveMenu().contains(durationFormat).click();
// click on Save
cy.get(".nc-col-create-or-edit-card").contains("Save").click();
// Verify if column exists.
//
cy.get(`th:contains(${columnName})`).should("exist");
};
// routine to delete column
//
const deleteColumnByName = (columnName) => {
// verify if column exists before delete
cy.get(`th:contains(${columnName})`).should("exist");
// delete opiton visible on mouse-over
cy.get(`th:contains(${columnName}) .mdi-menu-down`)
.trigger("mouseover")
.click();
// delete/ confirm on pop-up
cy.get(".nc-column-delete").click();
cy.getActiveModal().find("button:contains(Confirm)").click();
// validate if deleted (column shouldnt exist)
cy.get(`th:contains(${columnName})`).should("not.exist");
};
// routine to edit column
//
const editColumnByName = (oldName, newName, newDurationFormat) => {
// verify if column exists before delete
cy.get(`th:contains(${oldName})`).should("exist");
// delete opiton visible on mouse-over
cy.get(`th:contains(${oldName}) .mdi-menu-down`)
.trigger("mouseover")
.click();
// edit/ save on pop-up
cy.get(".nc-column-edit").click();
cy.get(".nc-column-name-input input").clear().type(newName);
// Configure Child table & column names
fetchParentFromLabel("Duration Format");
cy.getActiveMenu().contains(newDurationFormat).click();
cy.get(".nc-col-create-or-edit-card")
.contains("Save")
.click({ force: true });
cy.toastWait("Duration column updated successfully");
// validate if deleted (column shouldnt exist)
cy.get(`th:contains(${oldName})`).should("not.exist");
cy.get(`th:contains(${newName})`).should("exist");
};
const addDurationData = (colName, index, cellValue, expectedValue, isNewRow = false) => {
if (isNewRow) {
cy.get(".nc-add-new-row-btn:visible").should("exist");
cy.wait(500)
cy.get(".nc-add-new-row-btn").click({ force: true });
} else {
mainPage.getRow(index).find(".nc-row-expand-icon").click({ force: true });
}
cy.get(".duration-cell-wrapper > input").first().should('exist').type(cellValue);
cy.getActiveModal().find("button").contains("Save row").click({ force: true });
cy.toastWait("Row updated successfully");
mainPage.getCell(colName, index).find('input').then(($e) => {
expect($e[0].value).to.equal(expectedValue)
})
}
///////////////////////////////////////////////////
// Test case
{
// Duration: h:mm
it("Duration: h:mm", () => {
addDurationColumn("NC_DURATION_0", "h:mm (e.g. 1:23)");
addDurationData("NC_DURATION_0", 1, "1:30", "01:30", true);
addDurationData("NC_DURATION_0", 2, "30", "00:30", true);
addDurationData("NC_DURATION_0", 3, "60", "01:00", true);
addDurationData("NC_DURATION_0", 4, "80", "01:20", true);
addDurationData("NC_DURATION_0", 5, "12:34", "12:34", true);
addDurationData("NC_DURATION_0", 6, "15:130", "17:10", true);
addDurationData("NC_DURATION_0", 7, "123123", "2052:03", true);
});
it("Duration: Edit Column NC_DURATION_0", () => {
editColumnByName(
"NC_DURATION_0",
"NC_DURATION_EDITED_0",
"h:mm:ss (e.g. 3:45, 1:23:40)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_0");
});
}
{
// Duration: h:mm:ss
it("Duration: h:mm:ss", () => {
addDurationColumn("NC_DURATION_1", "h:mm:ss (e.g. 3:45, 1:23:40)");
addDurationData("NC_DURATION_1", 1, "11:22:33", "11:22:33");
addDurationData("NC_DURATION_1", 2, "1234", "00:20:34");
addDurationData("NC_DURATION_1", 3, "50", "00:00:50");
addDurationData("NC_DURATION_1", 4, "1:1111", "00:19:31");
addDurationData("NC_DURATION_1", 5, "1:11:1111", "01:29:31");
addDurationData("NC_DURATION_1", 6, "15:130", "00:17:10");
addDurationData("NC_DURATION_1", 7, "123123", "34:12:03");
});
it("Duration: Edit Column NC_DURATION_1", () => {
editColumnByName(
"NC_DURATION_1",
"NC_DURATION_EDITED_1",
"h:mm:ss.s (e.g. 3:34.6, 1:23:40.0)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_1");
});
}
{
// h:mm:ss.s
it("Duration: h:mm:ss.s", () => {
addDurationColumn("NC_DURATION_2", "h:mm:ss.s (e.g. 3:34.6, 1:23:40.0)");
addDurationData("NC_DURATION_2", 1, "1234", "00:20:34.0");
addDurationData("NC_DURATION_2", 2, "12:34", "00:12:34.0");
addDurationData("NC_DURATION_2", 3, "12:34:56", "12:34:56.0");
addDurationData("NC_DURATION_2", 4, "12:34:999", "12:50:39.0");
addDurationData("NC_DURATION_2", 5, "12:999:56", "28:39:56.0");
addDurationData("NC_DURATION_2", 6, "12:34:56.12", "12:34:56.1");
addDurationData("NC_DURATION_2", 7, "12:34:56.199", "12:34:56.2");
});
it("Duration: Edit Column NC_DURATION_2", () => {
editColumnByName(
"NC_DURATION_2",
"NC_DURATION_EDITED_2",
"h:mm:ss (e.g. 3:45, 1:23:40)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_2");
});
}
{
// h:mm:ss.ss
it("Duration: h:mm:ss.ss", () => {
addDurationColumn("NC_DURATION_3", "h:mm:ss.ss (e.g. 3.45.67, 1:23:40.00)");
addDurationData("NC_DURATION_3", 1, "1234", "00:20:34.00");
addDurationData("NC_DURATION_3", 2, "12:34", "00:12:34.00");
addDurationData("NC_DURATION_3", 3, "12:34:56", "12:34:56.00");
addDurationData("NC_DURATION_3", 4, "12:34:999", "12:50:39.00");
addDurationData("NC_DURATION_3", 5, "12:999:56", "28:39:56.00");
addDurationData("NC_DURATION_3", 6, "12:34:56.12", "12:34:56.12");
addDurationData("NC_DURATION_3", 7, "12:34:56.199", "12:34:56.20");
});
it("Duration: Edit Column NC_DURATION_3", () => {
editColumnByName(
"NC_DURATION_3",
"NC_DURATION_EDITED_3",
"h:mm:ss.ss (e.g. 3.45.67, 1:23:40.00)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_3");
});
}
{
// h:mm:ss.sss
it("Duration: h:mm:ss.sss", () => {
addDurationColumn("NC_DURATION_4", "h:mm:ss.sss (e.g. 3.45.678, 1:23:40.000)");
addDurationData("NC_DURATION_4", 1, "1234", "00:20:34.000");
addDurationData("NC_DURATION_4", 2, "12:34", "00:12:34.000");
addDurationData("NC_DURATION_4", 3, "12:34:56", "12:34:56.000");
addDurationData("NC_DURATION_4", 4, "12:34:999", "12:50:39.000");
addDurationData("NC_DURATION_4", 5, "12:999:56", "28:39:56.000");
addDurationData("NC_DURATION_4", 6, "12:34:56.12", "12:34:56.012");
addDurationData("NC_DURATION_4", 7, "12:34:56.199", "12:34:56.199");
});
it("Duration: Edit Column NC_DURATION_4", () => {
editColumnByName(
"NC_DURATION_4",
"NC_DURATION_EDITED_4",
"h:mm (e.g. 1:23)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_4");
});
}
});
};
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@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/>.
*
*/

2
scripts/cypress/integration/common/4e_form_view_share.js

@ -131,7 +131,7 @@ export const genTest = (apiType, dbType) => {
// all fields, barring removed field should exist
cy.get('[title="City"]').should("exist");
cy.get('[title="LastUpdate"]').should("exist");
cy.get('[title="CountryRead"]').should("exist");
cy.get('[title="Country"]').should("exist");
cy.get('[title="Address List"]').should("not.exist");
// order of LastUpdate & City field is retained

20
scripts/cypress/integration/common/4f_grid_view_share.js

@ -170,7 +170,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1013 Tabuk Boulevard,West Bengali,96203,158399646978,[object Object],2,,Kanchrapara,`,
`1892 Nabereznyje Telny Lane,Tutuila,28396,478229987054,[object Object],2,,Tafuna,`,
`1993 Tabuk Lane,Tamil Nadu,64221,648482415405,[object Object],2,,Tambaram,`,
@ -231,7 +231,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1993 Tabuk Lane,Tamil Nadu,64221,648482415405,[object Object],2,,Tambaram,`,
`1661 Abha Drive,Tamil Nadu,14400,270456873752,[object Object],1,,Pudukkottai,`,
];
@ -269,8 +269,8 @@ export const genTest = (apiType, dbType) => {
// verify column headers
cy.get('[data-col="Customer List"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
cy.get('[data-col="CityRead"]').should("exist");
cy.get('[data-col="StaffMMList"]').should("exist");
cy.get('[data-col="City"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
// has many field validation
mainPage
@ -308,17 +308,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > belongs to`, () => {
// belongs to field validation
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-arrow-expand")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".v-chip")
.contains("Kanchrapara")
.should("exist");
@ -327,17 +327,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > many to many`, () => {
// many-to-many field validation
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-plus")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-arrow-expand")
.click();

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

Loading…
Cancel
Save