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. 11
      .github/workflows/release-nightly-dev.yml
  4. 15
      .github/workflows/release-nocodb.yml
  5. 51
      .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. 290
      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. 4
      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. 6
      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. 5
      packages/nocodb/README.md
  58. 58
      packages/nocodb/package-lock.json
  59. 7
      packages/nocodb/package.json
  60. 10
      packages/nocodb/src/__tests__/restv2.test.ts
  61. 39
      packages/nocodb/src/lib/Noco.ts
  62. 86
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  63. 18
      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. 14
      packages/nocodb/src/lib/meta/api/columnApis.ts
  70. 12
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts
  71. 8
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  72. 15
      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. 6
      scripts/cypress/integration/common/1a_table_operations.js
  97. 2
      scripts/cypress/integration/common/1d_pg_table_view_drag_drop_reorder.js
  98. 2
      scripts/cypress/integration/common/1d_table_view_drag_drop_reorder.js
  99. 4
      scripts/cypress/integration/common/2a_table_with_belongs_to_colulmn.js
  100. 4
      scripts/cypress/integration/common/2b_table_with_m2m_column.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 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -61,7 +60,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-pg-restTableOps-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-pg-restViews-run-cache: cypress-pg-restViews-run-cache:
@ -94,7 +93,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -106,7 +104,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-pg-restViews-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-pg-restRoles-run-cache: cypress-pg-restRoles-run-cache:
@ -139,7 +137,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -151,7 +148,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-pg-restRoles-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-pg-restMisc-run-cache: cypress-pg-restMisc-run-cache:
@ -184,7 +181,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -196,7 +192,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-pg-restMisc-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-restTableOps-run-cache: cypress-restTableOps-run-cache:
@ -229,7 +225,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -241,7 +236,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-restTableOps-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-restViews-run-cache: cypress-restViews-run-cache:
@ -274,7 +269,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -286,7 +280,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-restViews-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-restRoles-run-cache: cypress-restRoles-run-cache:
@ -319,7 +313,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -331,7 +324,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-restRoles-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-restMisc-run-cache: cypress-restMisc-run-cache:
@ -364,7 +357,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:api:cache npm run start:api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -376,7 +368,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-restMisc-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-xcdb-restTableOps-run-cache: cypress-xcdb-restTableOps-run-cache:
@ -409,7 +401,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:xcdb-api:cache npm run start:xcdb-api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -421,7 +412,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-xcdb-restTableOps-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-xcdb-restViews-run-cache: cypress-xcdb-restViews-run-cache:
@ -454,7 +445,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:xcdb-api:cache npm run start:xcdb-api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -466,7 +456,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-xcdb-restViews-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-xcdb-restRoles-run-cache: cypress-xcdb-restRoles-run-cache:
@ -499,7 +489,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:xcdb-api:cache npm run start:xcdb-api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -511,7 +500,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-xcdb-restRoles-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
cypress-xcdb-restMisc-run-cache: cypress-xcdb-restMisc-run-cache:
@ -544,7 +533,6 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
start: | start: |
npm run build:common
npm run start:xcdb-api:cache npm run start:xcdb-api:cache
npm run start:web npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -556,7 +544,7 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: restTableOps-snapshots name: cypress-xcdb-restMisc-run-cache-snapshots
path: scripts/cypress/screenshots path: scripts/cypress/screenshots
retention-days: 2 retention-days: 2
# docker: # 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

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

@ -37,7 +37,7 @@ jobs:
outputs: outputs:
nightly_build_tag: ${{ steps.tag-step.outputs.NIGHTLY_BUILD_TAG }} nightly_build_tag: ${{ steps.tag-step.outputs.NIGHTLY_BUILD_TAG }}
is_daily: ${{ steps.tag-step.outputs.IS_DAILY }} is_daily: ${{ steps.tag-step.outputs.IS_DAILY }}
current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }} current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }}
# Build frontend and backend and publish to npm # Build frontend and backend and publish to npm
release-npm: release-npm:
needs: set-tag needs: set-tag
@ -48,6 +48,15 @@ jobs:
secrets: secrets:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}" 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 # Build docker image and push to docker hub
release-docker: release-docker:
needs: [set-tag, release-npm] needs: [set-tag, release-npm]

15
.github/workflows/release-nocodb.yml

@ -44,14 +44,14 @@ jobs:
# bump the version from PREV_TAG # bump the version from PREV_TAG
TARGET_TAG=$(echo ${PREV_TAG} | awk -F. -v OFS=. '{$NF += 1 ; print}') TARGET_TAG=$(echo ${PREV_TAG} | awk -F. -v OFS=. '{$NF += 1 ; print}')
fi fi
echo target version: ${TARGET_TAG} echo target version: ${TARGET_TAG}
echo previous version: ${PREV_TAG} echo previous version: ${PREV_TAG}
echo "::set-output name=target_tag::${TARGET_TAG}" echo "::set-output name=target_tag::${TARGET_TAG}"
echo "::set-output name=prev_tag::${PREV_TAG}" echo "::set-output name=prev_tag::${PREV_TAG}"
- name: Verify - name: Verify
run : | run : |
echo ${{ steps.process-input.outputs.target_tag }} echo ${{ steps.process-input.outputs.target_tag }}
# Merge develop to master # Merge develop to master
pr-to-master: pr-to-master:
@ -94,6 +94,15 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}" DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" 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 all issues with target tags 'Fixed' & 'Resolved'
close-fixed-issues: close-fixed-issues:
needs: [release-docker, process-input] needs: [release-docker, process-input]
@ -101,7 +110,7 @@ jobs:
with: with:
issue_label: 'Status: Fixed' issue_label: 'Status: Fixed'
version: ${{ needs.process-input.outputs.target_tag }} version: ${{ needs.process-input.outputs.target_tag }}
close-resolved-issues: close-resolved-issues:
needs: [close-fixed-issues, process-input] needs: [close-fixed-issues, process-input]
uses: ./.github/workflows/release-close-issue.yml uses: ./.github/workflows/release-close-issue.yml

51
.github/workflows/release-pr.yml

@ -50,7 +50,7 @@ jobs:
echo ${{ steps.tag-step.outputs.CURRENT_VERSION }} echo ${{ steps.tag-step.outputs.CURRENT_VERSION }}
outputs: outputs:
target_tag: ${{ steps.tag-step.outputs.TARGET_TAG }} target_tag: ${{ steps.tag-step.outputs.TARGET_TAG }}
current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }} current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }}
# Build, install, publish frontend and backend to npm # Build, install, publish frontend and backend to npm
release-npm: release-npm:
@ -77,13 +77,22 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}" DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" 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: # leave-comment:
# runs-on: 'ubuntu-latest' # runs-on: 'ubuntu-latest'
# needs: [release-docker, set-tag] # needs: [release-docker, set-tag]
# steps: # steps:
# - run: | # - run: |
# echo docker run -d -p 8888:8080 nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }} # echo docker run -d -p 8888:8080 nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
leave-comment: leave-comment:
if: ${{ github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false }} if: ${{ github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false }}
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
@ -96,7 +105,43 @@ jobs:
``` ```
docker run -d -p 8888:8080 nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }} 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-merged:
# if: github.event.pull_request.merged == true # if: github.event.pull_request.merged == true
# runs-on: ubuntu-latest # 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": { "scripts": {
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build", "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", "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: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": "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: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": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true 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: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: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:web": "cd ./packages/nc-gui; npm install; npm run dev", "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:run": "cypress run --config-file ./scripts/cypress/cypress.json",
"cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json", "cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json",
"cypress:clear": "cypress cache clear", "cypress:clear": "cypress cache clear",

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

@ -89,7 +89,7 @@
ref="name" ref="name"
v-model="project.title" v-model="project.title"
v-ge="['project', 'name']" v-ge="['project', 'name']"
:rules="form.titleRequiredRule" :rules="form.titleValidationRule"
:height="20" :height="20"
:label="$t('placeholder.projName')" :label="$t('placeholder.projName')"
autofocus autofocus
@ -453,14 +453,14 @@
" "
/> />
</v-col> </v-col>
<!-- todo : Schema name --> <!-- Schema name -->
<v-col <v-col
v-if="db.client === 'mssql' || db.client === 'pg'" v-if="db.client === 'mssql' || db.client === 'pg'"
cols="4" cols="4"
class="py-0" class="py-0"
> >
<v-text-field <v-text-field
v-model="schema" v-model="db.searchPath[0]"
:disabled="edit && enableDbEdit < 2" :disabled="edit && enableDbEdit < 2"
class="body-2 database-field" class="body-2 database-field"
:rules="form.requiredRule" :rules="form.requiredRule"
@ -891,7 +891,6 @@ export default {
layout: 'empty', layout: 'empty',
data() { data() {
return { return {
schema: 'public',
testSuccess: false, testSuccess: false,
projectCreated: false, projectCreated: false,
allSchemas: false, allSchemas: false,
@ -963,7 +962,10 @@ export default {
/** ************** START : form related ****************/ /** ************** START : form related ****************/
form: { form: {
portValidationRule: [v => /^\d+$/.test(v) || 'Not a valid port'], 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'], requiredRule: [v => !!v || 'Field is required'],
folderRequiredRule: [v => !!v || 'Folder path is required'] folderRequiredRule: [v => !!v || 'Folder path is required']
}, },
@ -1539,9 +1541,7 @@ export default {
this.projectReloading = true this.projectReloading = true
const con = projectJson.envs._noco.db[0] const con = projectJson.envs._noco.db[0]
if (con.client === 'pg' || con.client === 'mssql') { if (con.client !== 'pg' && con.client !== 'mssql' && 'searchPath' in con) {
con.searchPath = [this.schema]
} else if ('searchPath' in con) {
delete con.searchPath delete con.searchPath
} }
@ -1892,9 +1892,9 @@ export default {
}, },
onDatabaseTypeChanged(client, db1, index, env) { onDatabaseTypeChanged(client, db1, index, env) {
if (this.databaseNames[client] === 'mssql') { if (this.databaseNames[client] === 'mssql') {
this.schema = 'dbo' this.project.envs[env].db[index].searchPath[0] = 'dbo'
} else if (this.databaseNames[client] === 'pg') { } 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) { for (const env in this.project.envs) {

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

@ -315,6 +315,21 @@
</span> </span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </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-list-item
v-if="_isUIAllowed('excelQuickImport')" v-if="_isUIAllowed('excelQuickImport')"
v-t="['a:actions:import-excel']" v-t="['a:actions:import-excel']"
@ -370,6 +385,13 @@
@closeModal="quickImportModal = false" @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" /> <import-from-airtable v-if="airtableImportModal" v-model="airtableImportModal" />
</v-container> </v-container>
</template> </template>
@ -404,9 +426,11 @@ import GlobalAcl from '~/components/GlobalAcl'
import AuditTab from '~/components/project/AuditTab' import AuditTab from '~/components/project/AuditTab'
import QuickImport from '~/components/import/QuickImport' import QuickImport from '~/components/import/QuickImport'
import ImportFromAirtable from '~/components/import/ImportFromAirtable' import ImportFromAirtable from '~/components/import/ImportFromAirtable'
import JsonImport from '~/components/import/JSONImport'
export default { export default {
components: { components: {
JsonImport,
ImportFromAirtable, ImportFromAirtable,
SwaggerClient, SwaggerClient,
// Screensaver, // Screensaver,
@ -447,7 +471,8 @@ export default {
showScreensaver: false, showScreensaver: false,
quickImportModal: false, quickImportModal: false,
quickImportType: '', quickImportType: '',
airtableImportModal: false airtableImportModal: false,
jsonImportModal: false
} }
}, },
methods: { methods: {

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

@ -8,7 +8,6 @@
<div <div
v-t="['c:airtable-import:turbo-mode']" v-t="['c:airtable-import:turbo-mode']"
class="ml-2 mt-3 title pointer nc-btn-enable-turbo" class="ml-2 mt-3 title pointer nc-btn-enable-turbo"
@click="enableTurbo"
> >
🚀 🚀
</div> </div>
@ -86,6 +85,13 @@
hide-details hide-details
dense dense
/> />
<v-checkbox
v-model="syncSource.details.options.syncViews"
class="caption"
label="Import Secondary Views"
hide-details
dense
/>
<v-checkbox <v-checkbox
v-model="syncSource.details.options.syncRollup" v-model="syncSource.details.options.syncRollup"
class="caption" class="caption"
@ -225,7 +231,7 @@ export default {
apiKey: '', apiKey: '',
shareId: '', shareId: '',
options: { options: {
syncViews: false, syncViews: true,
syncData: true, syncData: true,
syncRollup: false, syncRollup: false,
syncLookup: true, syncLookup: true,
@ -327,7 +333,7 @@ export default {
apiKey: '', apiKey: '',
shareId: '', shareId: '',
options: { options: {
syncViews: false, syncViews: true,
syncData: true, syncData: true,
syncRollup: false, syncRollup: false,
syncLookup: true, syncLookup: true,
@ -350,10 +356,10 @@ export default {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
} }
}, },
enableTurbo() { // enableTurbo() {
this.$set(this.syncSource.details.options, 'syncViews', true) // this.$set(this.syncSource.details.options, 'syncViews', true)
this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000) // this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
}, // },
migrateSync(src) { migrateSync(src) {
if (!src.details?.options) { if (!src.details?.options) {
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" v-model="url"
hide-details="auto" hide-details="auto"
type="url" 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" class="caption"
outlined outlined
dense 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"> <v-btn v-t="['c:project:create:excel:load-url']" class="ml-3" color="primary" @click="loadUrl">
<!--Load--> <!--Load-->
@ -213,7 +220,12 @@ export default {
parserConfig: { parserConfig: {
maxRowsToParse: 500 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: { computed: {
@ -288,7 +300,7 @@ export default {
templateGenerator = new ExcelTemplateAdapter(name, val, this.parserConfig) templateGenerator = new ExcelTemplateAdapter(name, val, this.parserConfig)
break break
case 'url': 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 break
} }
await templateGenerator.init() await templateGenerator.init()
@ -322,11 +334,11 @@ export default {
if (this.quickImportType === 'excel') { if (this.quickImportType === 'excel') {
if (!/.*\.(xls|xlsx|xlsm|ods|ots)/.test(file.name)) { 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') { } else if (this.quickImportType === 'csv') {
if (!/.*\.(csv)/.test(file.name)) { 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) this._file(file)

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

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

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

@ -27,7 +27,25 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
} }
async init() { 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() { parse() {

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

@ -1,19 +1,19 @@
import ExcelTemplateAdapter from '~/components/import/templateParsers/ExcelTemplateAdapter' import ExcelTemplateAdapter from '~/components/import/templateParsers/ExcelTemplateAdapter'
export default class ExcelUrlTemplateAdapter extends ExcelTemplateAdapter { export default class ExcelUrlTemplateAdapter extends ExcelTemplateAdapter {
constructor(url, $store, parserConfig, $api) { constructor(url, $store, parserConfig, $api, quickImportType) {
const name = url.split('/').pop() const name = url.split('/').pop()
super(name, null, parserConfig) super(name, null, parserConfig)
this.url = url this.url = url
this.$api = $api this.$api = $api
this.$store = $store this.$store = $store
this.quickImportType = quickImportType
} }
async init() { async init() {
const data = await this.$api.utils.axiosRequestMake({ const data = await this.$api.utils.axiosRequestMake({
apiMeta: { apiMeta: {
url: this.url, url: this.url
responseType: 'arraybuffer'
} }
}) })
this.excelData = data.data 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 = [ const booleanOptions = [
{ checked: true, unchecked: false }, { checked: true, unchecked: false },
{ x: true, '': false }, { x: true, '': false },
@ -11,14 +14,24 @@ const booleanOptions = [
{ '✔': true, '': false }, { '✔': true, '': false },
{ enabled: true, disabled: false }, { enabled: true, disabled: false },
{ on: true, off: false }, { on: true, off: false },
{ done: true, '': false } { done: true, '': false },
{ true: true, false: false }
] ]
const aggBooleanOptions = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {}) 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 let options = booleanOptions
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
let val = col ? values[i][col] : values[i] const val = getColVal(values[i], col)
val = val === null || val === undefined ? '' : val
if (val === null || val === undefined || val.toString().trim() === '') {
continue
}
options = options.filter(v => val in v) options = options.filter(v => val in v)
if (!options.length) { if (!options.length) {
return false return false
@ -29,3 +42,40 @@ export const isCheckboxType = (values, col = '') => {
export const getCheckboxValue = (value) => { export const getCheckboxValue = (value) => {
return value && aggBooleanOptions[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: { methods: {
format() {
this.editor.getAction('editor.action.formatDocument').run()
},
resizeLayout() { resizeLayout() {
this.editor.layout(); 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" /> <date-time-cell v-else-if="isDateTime" :value="value" />
<time-cell v-else-if="isTime" :value="value" /> <time-cell v-else-if="isTime" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" read-only /> <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 /> <rating-cell v-else-if="isRating" :value="value" read-only />
<currency-cell v-else-if="isCurrency" :value="value" :column="column" /> <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 EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell' import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import CurrencyCell from '@/components/project/spreadsheet/components/cell/CurrencyCell' import CurrencyCell from '@/components/project/spreadsheet/components/cell/CurrencyCell'
import DurationCell from '@/components/project/spreadsheet/components/cell/DurationCell'
export default { export default {
name: 'TableCell', 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], mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'], props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: { computed: {

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

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

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

@ -35,6 +35,16 @@
v-on="$listeners" 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 <boolean-cell
v-else-if="isBoolean" v-else-if="isBoolean"
v-model="localState" v-model="localState"
@ -65,6 +75,7 @@
v-else-if="isTime" v-else-if="isTime"
v-model="localState" v-model="localState"
v-on="parentListeners" v-on="parentListeners"
@save="$emit('save')"
/> />
<date-time-picker-cell <date-time-picker-cell
@ -86,7 +97,6 @@
:is-form="isForm" :is-form="isForm"
:column="column" :column="column"
v-on="parentListeners" v-on="parentListeners"
@input="$emit('save')"
/> />
<json-editable-cell <json-editable-cell
@ -102,7 +112,6 @@
v-model="localState" v-model="localState"
:column="column" :column="column"
v-on="parentListeners" v-on="parentListeners"
@input="$emit('save')"
/> />
<set-list-cell <set-list-cell
v-else-if="isSet" 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 SetListEditableCell from '~/components/project/spreadsheet/components/editableCell/SetListEditableCell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell' import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell' import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import DurationCell from '~/components/project/spreadsheet/components/editableCell/DurationCell'
export default { export default {
name: 'EditableCell', name: 'EditableCell',
@ -167,7 +177,8 @@ export default {
TextAreaCell, TextAreaCell,
DateTimePickerCell, DateTimePickerCell,
TextCell, TextCell,
DatePickerCell DatePickerCell,
DurationCell
}, },
mixins: [cell], mixins: [cell],
props: { props: {
@ -199,10 +210,10 @@ export default {
if (val !== this.value) { if (val !== this.value) {
this.changed = true this.changed = true
this.$emit('input', val) this.$emit('input', val)
if (this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime || this.isDate) { if (this.isAutoSaved) {
this.syncData()
} else if (!this.isCurrency && !this.isEnum && !this.isSet) {
this.syncDataDebounce(this) this.syncDataDebounce(this)
} else if (!this.isManualSaved) {
this.saveData()
} }
} }
} }
@ -230,7 +241,7 @@ export default {
// this.$refs.input.focus(); // this.$refs.input.focus();
}, },
beforeDestroy() { beforeDestroy() {
if (this.changed && !(this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime)) { if (this.changed && this.isAutoSaved) {
this.changed = false this.changed = false
this.$emit('change') this.$emit('change')
} }
@ -242,6 +253,12 @@ export default {
this.changed = false this.changed = false
this.$emit('update') 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 // validate data type
if (parsedTree.callee.type === jsep.IDENTIFIER) { if (parsedTree.callee.type === jsep.IDENTIFIER) {
const expectedType = formulas[parsedTree.callee.name].type const expectedType = formulas[parsedTree.callee.name].type
if ( if (expectedType === formulaTypes.NUMERIC) {
expectedType === formulaTypes.NUMERIC ||
expectedType === formulaTypes.STRING
) {
parsedTree.arguments.map(arg => this.validateAgainstType(arg, expectedType, null, typeErrors)) parsedTree.arguments.map(arg => this.validateAgainstType(arg, expectedType, null, typeErrors))
} else if (expectedType === formulaTypes.DATE) { } else if (expectedType === formulaTypes.DATE) {
if (parsedTree.callee.name === 'DATEADD') { 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> </template>
<div class="d-flex flex-column justify-center" @click.stop> <div class="d-flex flex-column justify-center" @click.stop>
<v-time-picker v-model="localState" v-on="parentListeners" /> <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 --> <!-- Save -->
{{ $t('general.save') }} {{ $t('general.save') }}
</v-btn> </v-btn>
@ -15,7 +15,6 @@
<script> <script>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { MysqlUi } from 'nocodb-sdk'
export default { export default {
name: 'TimePickerCell', name: 'TimePickerCell',

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

@ -67,8 +67,34 @@ export default {
}, },
isCurrency() { isCurrency() {
return this.uiDatatype === 'Currency' 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 Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@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 * @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 continue
} }
if ( if (!isVirtualCol(column) && (((column.rqd || column.notnull) && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required)) {
!column.virtual &&
(((column.rqd || column.notnull) && !column.cdf) ||
(column.pk && !(column.ai || column.default)) ||
this.localParams.fields[column.title].required)
) {
obj.localState[column.title] = { required } obj.localState[column.title] = { required }
} else if (column.bt) { } else if (column.uidt === UITypes.LinkToAnotherRecord && column.colOptions && column.colOptions.type === RelationTypes.BELONGS_TO) {
const col = this.meta.columns.find( const col = this.meta.columns.find(c => c.id === column.colOptions.fk_child_column_id)
c => c.column_name === column.bt.column_name
) if ((col && col.rqd && !col.cdf) || column.required) {
if ( if (col) { obj.virtual[column.title] = { required } }
(col.rqd && !col.default) ||
this.localParams.fields[column.title].required
) {
obj.localState[col.title] = { required }
} }
} else if ( } else if (isVirtualCol(column) && column.required) {
column.virtual &&
this.localParams.fields[column.title].required &&
(column.mm || column.hm)
) {
obj.virtual[column.title] = { obj.virtual[column.title] = {
minLength: minLength(1), minLength: minLength(1),
required required
} }
} }
} }
return obj return obj
}, },
computed: { 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/>.
*
*/

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

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

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

@ -28,7 +28,7 @@
"monaco-editor": "^0.19.3", "monaco-editor": "^0.19.3",
"monaco-themes": "^0.2.5", "monaco-themes": "^0.2.5",
"nano-assign": "^1.0.1", "nano-assign": "^1.0.1",
"nocodb-sdk": "0.91.10", "nocodb-sdk": "file:../nocodb-sdk",
"nuxt": "^2.14.0", "nuxt": "^2.14.0",
"odometer": "^0.4.8", "odometer": "^0.4.8",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",
@ -76,7 +76,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.91.10", "version": "0.91.10",
"extraneous": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -10899,16 +10898,8 @@
} }
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.91.10", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz", "link": true
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"engines": {
"node": ">=10"
}
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.7", "version": "2.6.7",
@ -26326,12 +26317,33 @@
} }
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.91.10", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"requires": { "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", "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": { "node-fetch": {

2
packages/nc-gui/package.json

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

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

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

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

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

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

@ -1,3 +1,5 @@
import { validatePassword } from 'nocodb-sdk'
export default { export default {
data: () => ({ data: () => ({
passwordProgress: 0, passwordProgress: 0,
@ -20,54 +22,62 @@ export default {
return this.formUtil.progressColorValue return this.formUtil.progressColorValue
}, },
PasswordValidate(p) { PasswordValidate(p) {
if (!p) { const { error, progress, valid } = validatePassword(p)
this.passwordProgress = 0 if (valid) { return true }
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 this.formUtil.passwordProgress = progress
// console.log('progress', progress);
// console.log('color', this.progressColor(this.formUtil.passwordProgress));
this.progressColorValue = this.progressColor(this.formUtil.passwordProgress) this.progressColorValue = this.progressColor(this.formUtil.passwordProgress)
this.formUtil.passwordValidateMsg = msg this.formUtil.passwordValidateMsg = error
return error
// console.log('msg', msg, validation); // if (!p) {
// this.passwordProgress = 0
return validation // 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: [
// Password is required // Password is required
v => !!v || this.$t('msg.error.signUpRules.passwdRequired'), 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')
] ]
}, },
formUtil: { formUtil: {

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

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

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

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

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

@ -7390,16 +7390,16 @@
} }
}, },
"node_modules/got": { "node_modules/got": {
"version": "11.8.2", "version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": { "dependencies": {
"@sindresorhus/is": "^4.0.0", "@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5", "@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1", "@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0", "@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3", "cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1", "cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0", "decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2", "http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0", "lowercase-keys": "^2.0.0",
@ -13804,9 +13804,9 @@
} }
}, },
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.7.2", "version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.0.4", "version": "1.0.4",
@ -22650,16 +22650,16 @@
} }
}, },
"got": { "got": {
"version": "11.8.2", "version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": { "requires": {
"@sindresorhus/is": "^4.0.0", "@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5", "@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1", "@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0", "@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3", "cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1", "cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0", "decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2", "http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0", "lowercase-keys": "^2.0.0",
@ -27611,9 +27611,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
}, },
"shell-quote": { "shell-quote": {
"version": "1.7.2", "version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
}, },
"side-channel": { "side-channel": {
"version": "1.0.4", "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 | | Category | Method | Tag | Function Name | Path |
|---|---|---|---|---| |---|---|---|---|---|
| Auth | Post | auth | signup | /api/v1/db/auth/user/signup | | Auth | Post | auth | signup | /api/v1/auth/user/signup |
| Auth | Post | auth | signin | /api/v1/db/auth/user/signin | | Auth | Post | auth | signin | /api/v1/auth/user/signin |
| Auth | Get | auth | me | /api/v1/db/auth/user/me | | Auth | Get | auth | me | /api/v1/auth/user/me |
| Auth | Post | auth | passwordForgot | /api/v1/db/auth/password/forgot | | Auth | Post | auth | passwordForgot | /api/v1/auth/password/forgot |
| Auth | Post | auth | passwordChange | /api/v1/db/auth/password/change | | Auth | Post | auth | passwordChange | /api/v1/auth/password/change |
| Auth | Post | auth | passwordReset | /api/v1/db/auth/password/reset/{token} | | Auth | Post | auth | passwordReset | /api/v1/auth/password/reset/{token} |
| Auth | Post | auth | tokenRefresh | /api/v1/db/auth/token/refresh | | Auth | Post | auth | tokenRefresh | /api/v1/auth/token/refresh |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/db/auth/token/validate/{token} | | Auth | Post | auth | passwordResetTokenValidate | /api/v1/auth/token/validate/{token} |
| Auth | Post | auth | emailValidate | /api/v1/db/auth/email/validate/{email} | | Auth | Post | auth | emailValidate | /api/v1/auth/email/validate/{email} |
### Public APIs ### 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 | Delete| utils | cacheDelete | /api/v1/db/meta/cache |
| Meta | Post | utils | testConnection | /api/v1/db/meta/projects/connection/test | | Meta | Post | utils | testConnection | /api/v1/db/meta/projects/connection/test |
| Meta | Get | utils | appInfo | /api/v1/db/meta/nocodb/info | | 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 ## 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 | | Operation | Meaning | Example |
|---|---|---| |---|---|---|
| eq | equal | (colName,eq,colValue) | | 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) | | gt | greater than | (colName,gt,colValue) |
| ge | greater or equal | (colName,ge,colValue) | | ge | greater or equal | (colName,ge,colValue) |
| lt | less than | (colName,lt,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 npm start
``` ```
### Homebrew
```bash
brew tap nocodb/nocodb
brew install nocodb
nocodb
```
### Docker ### Docker
If you are a Docker user, you may try this way! 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_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 | 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 | | | 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) ### AWS ECS (Fargate)

4
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. 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 Your password has at least 8 letters with one uppercase, one number and one special letter
</alert> </alert>
@ -98,4 +98,4 @@ Tip 3: You can click Edit Connection JSON and specify the schema you want to use
Click `Test Database Connection` to see if the connection can be established or not. NocoDB creates a new **empty database** with specified parameters if the database doesn't exist. Click `Test Database Connection` to see if the connection can be established or not. NocoDB creates a new **empty database** with specified parameters if the database doesn't exist.
![image](https://user-images.githubusercontent.com/35857179/163136039-ad521d74-6996-4173-84ba-cfc55392c3b7.png) ![image](https://user-images.githubusercontent.com/35857179/163136039-ad521d74-6996-4173-84ba-cfc55392c3b7.png)

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" title: "Primary value"
description: "Primary value" description: "Primary value"
position: 575 position: 580
category: "Product" category: "Product"
menuTitle: "Primary value" menuTitle: "Primary value"
--- ---

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

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

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

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

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

@ -5,5 +5,7 @@ export * from './lib/sqlUi';
export * from './lib/globals'; export * from './lib/globals';
export * from './lib/helperFunctions'; export * from './lib/helperFunctions';
export * from './lib/formulaHelpers'; export * from './lib/formulaHelpers';
export {default as UITypes, isVirtualCol} from './lib/UITypes'; export { default as UITypes, isVirtualCol } from './lib/UITypes';
export {default as CustomAPI} from './lib/CustomAPI'; 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 * @tags Auth
* @name Signup * @name Signup
* @summary Signup * @summary Signup
* @request POST:/api/v1/db/auth/user/signup * @request POST:/api/v1/auth/user/signup
* @response `200` `{ token?: string }` OK * @response `200` `{ token?: string }` OK
* @response `400` `{ msg?: string }` Bad Request * @response `400` `{ msg?: string }` Bad Request
* @response `401` `void` Unauthorized * @response `401` `void` Unauthorized
@ -816,7 +816,7 @@ export class Api<
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<{ token?: string }, { msg?: string } | void>({ this.request<{ token?: string }, { msg?: string } | void>({
path: `/api/v1/db/auth/user/signup`, path: `/api/v1/auth/user/signup`,
method: 'POST', method: 'POST',
body: data, body: data,
format: 'json', format: 'json',
@ -829,7 +829,7 @@ export class Api<
* @tags Auth * @tags Auth
* @name Signin * @name Signin
* @summary Signin * @summary Signin
* @request POST:/api/v1/db/auth/user/signin * @request POST:/api/v1/auth/user/signin
* @response `200` `{ token?: string }` OK * @response `200` `{ token?: string }` OK
* @response `400` `{ msg?: string }` Bad Request * @response `400` `{ msg?: string }` Bad Request
*/ */
@ -838,7 +838,7 @@ export class Api<
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<{ token?: string }, { msg?: string }>({ this.request<{ token?: string }, { msg?: string }>({
path: `/api/v1/db/auth/user/signin`, path: `/api/v1/auth/user/signin`,
method: 'POST', method: 'POST',
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
@ -852,12 +852,12 @@ export class Api<
* @tags Auth * @tags Auth
* @name Me * @name Me
* @summary User info * @summary User info
* @request GET:/api/v1/db/auth/user/me * @request GET:/api/v1/auth/user/me
* @response `200` `UserInfoType` OK * @response `200` `UserInfoType` OK
*/ */
me: (query?: { project_id?: string }, params: RequestParams = {}) => me: (query?: { project_id?: string }, params: RequestParams = {}) =>
this.request<UserInfoType, any>({ this.request<UserInfoType, any>({
path: `/api/v1/db/auth/user/me`, path: `/api/v1/auth/user/me`,
method: 'GET', method: 'GET',
query: query, query: query,
format: 'json', format: 'json',
@ -870,13 +870,13 @@ export class Api<
* @tags Auth * @tags Auth
* @name PasswordForgot * @name PasswordForgot
* @summary Password forgot * @summary Password forgot
* @request POST:/api/v1/db/auth/password/forgot * @request POST:/api/v1/auth/password/forgot
* @response `200` `void` OK * @response `200` `void` OK
* @response `401` `void` Unauthorized * @response `401` `void` Unauthorized
*/ */
passwordForgot: (data: { email?: string }, params: RequestParams = {}) => passwordForgot: (data: { email?: string }, params: RequestParams = {}) =>
this.request<void, void>({ this.request<void, void>({
path: `/api/v1/db/auth/password/forgot`, path: `/api/v1/auth/password/forgot`,
method: 'POST', method: 'POST',
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
@ -889,7 +889,7 @@ export class Api<
* @tags Auth * @tags Auth
* @name PasswordChange * @name PasswordChange
* @summary Password change * @summary Password change
* @request POST:/api/v1/db/auth/password/change * @request POST:/api/v1/auth/password/change
* @response `200` `{ msg?: string }` OK * @response `200` `{ msg?: string }` OK
* @response `400` `{ msg?: string }` Bad request * @response `400` `{ msg?: string }` Bad request
*/ */
@ -898,7 +898,7 @@ export class Api<
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<{ msg?: string }, { msg?: string }>({ this.request<{ msg?: string }, { msg?: string }>({
path: `/api/v1/db/auth/password/change`, path: `/api/v1/auth/password/change`,
method: 'POST', method: 'POST',
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
@ -912,12 +912,12 @@ export class Api<
* @tags Auth * @tags Auth
* @name PasswordResetTokenValidate * @name PasswordResetTokenValidate
* @summary Reset token verify * @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 * @response `200` `void` OK
*/ */
passwordResetTokenValidate: (token: string, params: RequestParams = {}) => passwordResetTokenValidate: (token: string, params: RequestParams = {}) =>
this.request<void, any>({ this.request<void, any>({
path: `/api/v1/db/auth/token/validate/${token}`, path: `/api/v1/auth/token/validate/${token}`,
method: 'POST', method: 'POST',
...params, ...params,
}), }),
@ -928,12 +928,12 @@ export class Api<
* @tags Auth * @tags Auth
* @name EmailValidate * @name EmailValidate
* @summary Verify email * @summary Verify email
* @request POST:/api/v1/db/auth/email/validate/{token} * @request POST:/api/v1/auth/email/validate/{token}
* @response `200` `void` OK * @response `200` `void` OK
*/ */
emailValidate: (token: string, params: RequestParams = {}) => emailValidate: (token: string, params: RequestParams = {}) =>
this.request<void, any>({ this.request<void, any>({
path: `/api/v1/db/auth/email/validate/${token}`, path: `/api/v1/auth/email/validate/${token}`,
method: 'POST', method: 'POST',
...params, ...params,
}), }),
@ -944,7 +944,7 @@ export class Api<
* @tags Auth * @tags Auth
* @name PasswordReset * @name PasswordReset
* @summary Password reset * @summary Password reset
* @request POST:/api/v1/db/auth/password/reset/{token} * @request POST:/api/v1/auth/password/reset/{token}
* @response `200` `void` OK * @response `200` `void` OK
*/ */
passwordReset: ( passwordReset: (
@ -953,7 +953,7 @@ export class Api<
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<void, any>({ this.request<void, any>({
path: `/api/v1/db/auth/password/reset/${token}`, path: `/api/v1/auth/password/reset/${token}`,
method: 'POST', method: 'POST',
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
@ -966,12 +966,12 @@ export class Api<
* @tags Auth * @tags Auth
* @name TokenRefresh * @name TokenRefresh
* @summary Refresh token * @summary Refresh token
* @request POST:/api/v1/db/auth/token/refresh * @request POST:/api/v1/auth/token/refresh
* @response `200` `void` OK * @response `200` `void` OK
*/ */
tokenRefresh: (params: RequestParams = {}) => tokenRefresh: (params: RequestParams = {}) =>
this.request<void, any>({ this.request<void, any>({
path: `/api/v1/db/auth/token/refresh`, path: `/api/v1/auth/token/refresh`,
method: 'POST', method: 'POST',
...params, ...params,
}), }),
@ -3197,12 +3197,28 @@ export class Api<
* *
* @tags Utils * @tags Utils
* @name AppVersion * @name AppVersion
* @request GET:/api/v1/db/meta/nocodb/version * @request GET:/api/v1/version
* @response `200` `any` OK * @response `200` `any` OK
*/ */
appVersion: (params: RequestParams = {}) => appVersion: (params: RequestParams = {}) =>
this.request<any, any>({ 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', method: 'GET',
format: 'json', format: 'json',
...params, ...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, pk: false,
un: false, un: false,
ai: false, ai: false,
au: true,
cdf: 'GETDATE()', cdf: 'GETDATE()',
clen: 45, clen: 45,
np: null, np: null,
@ -919,7 +920,9 @@ export class MssqlUi {
// if (1) { // if (1) {
col.altered = col.altered || 2; col.altered = col.altered || 2;
// } // }
if (col.au) {
col.cdf = 'GETDATE()';
}
// if (!col.ai) { // if (!col.ai) {
// col.dtx = 'specificType' // col.dtx = 'specificType'
// } else { // } else {
@ -1144,7 +1147,7 @@ export class MssqlUi {
colProp.dt = 'double'; colProp.dt = 'double';
break; break;
case 'Duration': case 'Duration':
colProp.dt = 'int'; colProp.dt = 'decimal';
break; break;
case 'Rating': case 'Rating':
colProp.dt = 'int'; colProp.dt = 'int';

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

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

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

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

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

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

5
packages/nocodb/README.md

@ -1,10 +1,11 @@
# nocodb # Nocodb
## Running locally ## Running locally
Even though this package is a backend project, you can still visit the dashboard as it includes ``nc-lib-gui``. Even though this package is a backend project, you can still visit the dashboard as it includes ``nc-lib-gui``.
``` ```
npm install npm install
npm run watch:run npm run watch:run
# open localhost:8080/dashboard in browser # open localhost:8080/dashboard in browser
@ -18,4 +19,4 @@ If you wish to combine the frontend and backend together in your local devlopmen
"nc-lib-gui": "file:../nc-lib-gui" "nc-lib-gui": "file:../nc-lib-gui"
``` ```
In this case, whenever there is any changes made in frontend, you need to run ``npm run build:copy`` under ``packages/nc-gui/``. In this case, whenever there is any changes made in frontend, you need to run ``npm run build:copy`` under ``packages/nc-gui/``.

58
packages/nocodb/package-lock.json generated

@ -70,11 +70,11 @@
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-common": "0.0.6", "nc-common": "0.0.6",
"nc-help": "0.2.61", "nc-help": "0.2.67",
"nc-lib-gui": "0.91.10", "nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.91.10", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"ora": "^4.0.4", "ora": "^4.0.4",
@ -232,7 +232,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.91.10", "version": "0.91.10",
"extraneous": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -16353,9 +16352,9 @@
} }
}, },
"node_modules/nc-help": { "node_modules/nc-help": {
"version": "0.2.61", "version": "0.2.67",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.61.tgz", "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.67.tgz",
"integrity": "sha512-YvFD0EjhwLs36U4GqIg0uxYWBurc39Wt7Hi7pd4/Lh1A/hmpWl6eZD4k9gk2FqqtGMlR/7e5UOmFYrRuvtzkTw==", "integrity": "sha512-O9eXHrpO0dBdFv6zUZAos+63JZEGhZ2lG+MduGZ+/BL7M5b0qU7d9b95Pmgq6Gd5wO3txT/7x7uPBHZxeSgvHQ==",
"dependencies": { "dependencies": {
"@rudderstack/rudder-sdk-node": "^1.1.3", "@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -16571,16 +16570,8 @@
"dev": true "dev": true
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.91.10", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz", "link": true
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"engines": {
"node": ">=10"
}
}, },
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "2.0.0", "version": "2.0.0",
@ -38049,9 +38040,9 @@
"integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw==" "integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw=="
}, },
"nc-help": { "nc-help": {
"version": "0.2.61", "version": "0.2.67",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.61.tgz", "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.67.tgz",
"integrity": "sha512-YvFD0EjhwLs36U4GqIg0uxYWBurc39Wt7Hi7pd4/Lh1A/hmpWl6eZD4k9gk2FqqtGMlR/7e5UOmFYrRuvtzkTw==", "integrity": "sha512-O9eXHrpO0dBdFv6zUZAos+63JZEGhZ2lG+MduGZ+/BL7M5b0qU7d9b95Pmgq6Gd5wO3txT/7x7uPBHZxeSgvHQ==",
"requires": { "requires": {
"@rudderstack/rudder-sdk-node": "^1.1.3", "@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -38231,12 +38222,33 @@
"dev": true "dev": true
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.91.10", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"requires": { "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", "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": { "node-addon-api": {

7
packages/nocodb/package.json

@ -69,6 +69,7 @@
"watch:serve": "nodemon -e ts -w ./build -x npm run debug-local ", "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": "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": "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: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\"", "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", "run": "ts-node src/run/docker",
@ -153,11 +154,11 @@
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-common": "0.0.6", "nc-common": "0.0.6",
"nc-help": "0.2.61", "nc-help": "0.2.67",
"nc-lib-gui": "0.91.10", "nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.91.10", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"ora": "^4.0.4", "ora": "^4.0.4",
@ -257,4 +258,4 @@
"**/*.spec.js" "**/*.spec.js"
] ]
} }
} }

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

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

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

@ -42,6 +42,7 @@ import { Tele } from 'nc-help';
import * as http from 'http'; import * as http from 'http';
import weAreHiring from './utils/weAreHiring'; import weAreHiring from './utils/weAreHiring';
import getInstance from './utils/getInstance'; import getInstance from './utils/getInstance';
import initAdminFromEnv from './meta/api/userApi/initAdminFromEnv';
const log = debug('nc:app'); const log = debug('nc:app');
require('dotenv').config(); require('dotenv').config();
@ -186,8 +187,8 @@ export default class Noco {
} }
await Noco._ncMeta.metaInit(); await Noco._ncMeta.metaInit();
await this.initJwt();
await this.readOrGenJwtSecret(); await initAdminFromEnv();
await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta }); await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta });
@ -488,20 +489,28 @@ export default class Noco {
} }
} }
private async readOrGenJwtSecret(): Promise<any> { private async initJwt(): Promise<any> {
if (this.config?.auth?.jwt && !this.config.auth.jwt.secret) { if (this.config?.auth?.jwt) {
let secret = ( if (!this.config.auth.jwt.secret) {
await Noco._ncMeta.metaGet('', '', 'nc_store', { let secret = (
key: 'nc_auth_jwt_secret' await Noco._ncMeta.metaGet('', '', 'nc_store', {
}) key: 'nc_auth_jwt_secret'
)?.value; })
if (!secret) { )?.value;
await Noco._ncMeta.metaInsert('', '', 'nc_store', { if (!secret) {
key: 'nc_auth_jwt_secret', await Noco._ncMeta.metaInsert('', '', 'nc_store', {
value: secret = uuidv4() key: 'nc_auth_jwt_secret',
}); value: secret = uuidv4()
});
}
this.config.auth.jwt.secret = secret;
}
this.config.auth.jwt.options = this.config.auth.jwt.options || {};
if (!this.config.auth.jwt.options?.expiresIn) {
this.config.auth.jwt.options.expiresIn =
process.env.NC_JWT_EXPIRES_IN ?? '10h';
} }
this.config.auth.jwt.secret = secret;
} }
let serverId = ( let serverId = (
await Noco._ncMeta.metaGet('', '', 'nc_store', { await Noco._ncMeta.metaGet('', '', 'nc_store', {

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

18
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 FormulaColumn from '../../../../models/FormulaColumn';
import { RelationTypes, UITypes } from 'nocodb-sdk'; import { RelationTypes, UITypes } from 'nocodb-sdk';
// import LookupColumn from '../../../models/LookupColumn'; // import LookupColumn from '../../../models/LookupColumn';
import { sanitize } from './helpers/sanitize';
export default async function conditionV2( export default async function conditionV2(
conditionObj: Filter | Filter[], conditionObj: Filter | Filter[],
@ -203,11 +204,13 @@ const parseConditionV2 = async (
filter.comparison_op === 'notempty' filter.comparison_op === 'notempty'
) )
filter.value = ''; filter.value = '';
let field = customWhereClause let field = sanitize(
? filter.value customWhereClause
: alias ? filter.value
? `${alias}.${column.column_name}` : alias
: column.column_name; ? `${alias}.${column.column_name}`
: column.column_name
);
let val = customWhereClause ? customWhereClause : filter.value; let val = customWhereClause ? customWhereClause : filter.value;
return qb => { return qb => {
@ -216,12 +219,13 @@ const parseConditionV2 = async (
qb = qb.where(field, val); qb = qb.where(field, val);
break; break;
case 'neq': case 'neq':
case 'not':
qb = qb.whereNot(field, val); qb = qb.whereNot(field, val);
break; break;
case 'like': case 'like':
if (column.uidt === UITypes.Formula) { if (column.uidt === UITypes.Formula) {
[field, val] = [val, field]; [field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%') val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
} else { } else {
val = `%${val}%`; val = `%${val}%`;
} }
@ -234,7 +238,7 @@ const parseConditionV2 = async (
case 'nlike': case 'nlike':
if (column.uidt === UITypes.Formula) { if (column.uidt === UITypes.Formula) {
[field, val] = [val, field]; [field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%') val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
} else { } else {
val = `%${val}%`; 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)) && (!fields?.length || fields.includes(col.title)) &&
value value
: fields?.length : fields?.length
? fields.includes(col.title) ? fields.includes(col.title) && value
: value : value
}; };
}, Promise.resolve({})); }, 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 formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import FormulaColumn from '../../../../models/FormulaColumn'; import FormulaColumn from '../../../../models/FormulaColumn';
import { RelationTypes, UITypes } from 'nocodb-sdk'; import { RelationTypes, UITypes } from 'nocodb-sdk';
import { sanitize } from './helpers/sanitize';
export default async function sortV2( export default async function sortV2(
sortList: Sort[], sortList: Sort[],
@ -205,7 +206,7 @@ export default async function sortV2(
} }
break; break;
default: default:
qb.orderBy(`${column.column_name}`, sort.direction || 'asc'); qb.orderBy(sanitize(column.column_name), sort.direction || 'asc');
break; break;
} }
} }

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

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

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

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

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

@ -57,7 +57,7 @@ async function createHmAndBtColumn(
{ {
const title = getUniqueColumnAliasName( const title = getUniqueColumnAliasName(
await child.getColumns(), await child.getColumns(),
type === 'bt' ? alias : `${parent.title}Read` type === 'bt' ? alias : `${parent.title}`
); );
await Column.insert<LinkToAnotherRecordColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
title, title,
@ -79,7 +79,7 @@ async function createHmAndBtColumn(
{ {
const title = getUniqueColumnAliasName( const title = getUniqueColumnAliasName(
await parent.getColumns(), await parent.getColumns(),
type === 'hm' ? alias : `${child.title}List` type === 'hm' ? alias : `${child.title} List`
); );
await Column.insert({ await Column.insert({
title, title,
@ -427,7 +427,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({ await Column.insert({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
await child.getColumns(), await child.getColumns(),
`${child.title}MMList` `${parent.title} List`
), ),
uidt: UITypes.LinkToAnotherRecord, uidt: UITypes.LinkToAnotherRecord,
type: 'mm', type: 'mm',
@ -447,7 +447,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({ await Column.insert({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
await parent.getColumns(), await parent.getColumns(),
req.body.title ?? `${parent.title}MMList` req.body.title ?? `${child.title} List`
), ),
uidt: UITypes.LinkToAnotherRecord, uidt: UITypes.LinkToAnotherRecord,
@ -503,6 +503,12 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
default: default:
{ {
colBody = getColumnPropsFromUIDT(colBody, base); 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 = { const tableUpdateBody = {
...table, ...table,
tn: table.table_name, tn: table.table_name,

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

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

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

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

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

@ -95,6 +95,10 @@ async function projectCreate(req: Request<any, any>, res) {
projectBody.is_meta = false; projectBody.is_meta = false;
} }
if (projectBody?.title.length > 50) {
NcError.badRequest('Project title exceeds 50 characters');
}
if (await Project.getByTitle(projectBody?.title)) { if (await Project.getByTitle(projectBody?.title)) {
NcError.badRequest('Project title already in use'); NcError.badRequest('Project title already in use');
} }
@ -211,7 +215,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord, uidt: UITypes.LinkToAnotherRecord,
type: 'hm', type: 'hm',
hm, hm,
title: `${hm.title}List` title: `${hm.title} List`
}; };
}), }),
...belongsTo.map(bt => { ...belongsTo.map(bt => {
@ -226,7 +230,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord, uidt: UITypes.LinkToAnotherRecord,
type: 'bt', type: 'bt',
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); cost = Math.min(120 * userCount, 36000);
} }
Tele.event({
event: 'a:project:cost',
data: {
cost
}
});
res.json({ 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 Noco from '../../Noco';
import { PluginCategory } from 'nocodb-sdk'; import { PluginCategory } from 'nocodb-sdk';
import { metaApiMetrics } from '../helpers/apiMetrics'; import { metaApiMetrics } from '../helpers/apiMetrics';
import { randomTokenString } from '../helpers/stringHelpers';
async function userList(req, res) { async function userList(req, res) {
res.json({ res.json({
@ -101,7 +102,8 @@ async function userInvite(req, res, next): Promise<any> {
invite_token, invite_token,
invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
email, email,
roles: 'user' roles: 'user',
token_version: randomTokenString()
}); });
// add user to project // 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`]: { [`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/groupby`]: {
get: { get: {
summary: `${ctx.tableName} groupby`, summary: `${ctx.tableName} groupby`,

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

@ -1,4 +1,5 @@
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
import crypto from 'crypto';
import User from '../../../models/User'; import User from '../../../models/User';
import { NcConfig } from '../../../../interface/config'; import { NcConfig } from '../../../../interface/config';
@ -16,3 +17,7 @@ export function genJwt(user: User, config: NcConfig) {
config.auth.jwt.options 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 ProjectUser from '../../../models/ProjectUser';
import { promisify } from 'util'; import { promisify } from 'util';
import { Strategy as CustomStrategy } from 'passport-custom'; import { Strategy as CustomStrategy } from 'passport-custom';
import { Strategy } from 'passport-jwt';
import passport from 'passport'; 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 AuthTokenStrategy } from 'passport-auth-token';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { randomTokenString } from '../../helpers/stringHelpers';
const PassportLocalStrategy = require('passport-local').Strategy; const PassportLocalStrategy = require('passport-local').Strategy;
const ExtractJwt = passportJWT.ExtractJwt;
const JwtStrategy = passportJWT.Strategy;
const jwtOptions = { const jwtOptions = {
expiresIn: process.env.NC_JWT_EXPIRES_IN ?? '10h',
jwtFromRequest: ExtractJwt.fromHeader('xc-auth') jwtFromRequest: ExtractJwt.fromHeader('xc-auth')
}; };
@ -83,7 +83,7 @@ export function initStrategies(router): void {
}); });
passport.use( passport.use(
new Strategy( new JwtStrategy(
{ {
secretOrKey: Noco.getConfig().auth.jwt.secret, secretOrKey: Noco.getConfig().auth.jwt.secret,
...jwtOptions, ...jwtOptions,
@ -102,7 +102,11 @@ export function initStrategies(router): void {
); );
if (cachedVal) { 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(new Error('Token Expired. Please login again.'));
} }
return done(null, cachedVal); return done(null, cachedVal);
@ -110,7 +114,11 @@ export function initStrategies(router): void {
User.getByEmail(jwtPayload?.email) User.getByEmail(jwtPayload?.email)
.then(async user => { .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.')); return done(new Error('Token Expired. Please login again.'));
} }
if (req.ncProjectId) { if (req.ncProjectId) {
@ -266,7 +274,8 @@ export function initStrategies(router): void {
password: '', password: '',
salt, salt,
roles, roles,
email_verified: true email_verified: true,
token_version: randomTokenString()
}); });
return done(null, user); 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: {}, methods: {},
async created() { async created() {
try { 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; this.valid = !!valid;
} catch (e) { } catch (e) {
this.valid = false; this.valid = false;

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

@ -1,5 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { TableType } from 'nocodb-sdk'; import { TableType, validatePassword } from 'nocodb-sdk';
import catchError, { NcError } from '../../helpers/catchError'; import catchError, { NcError } from '../../helpers/catchError';
const { isEmail } = require('validator'); const { isEmail } = require('validator');
import * as ejs from 'ejs'; import * as ejs from 'ejs';
@ -11,7 +11,6 @@ import { Tele } from 'nc-help';
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
import Audit from '../../../models/Audit'; import Audit from '../../../models/Audit';
import crypto from 'crypto';
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2'; import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2';
import passport from 'passport'; import passport from 'passport';
@ -20,6 +19,7 @@ import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import { MetaTable } from '../../../utils/globals'; import { MetaTable } from '../../../utils/globals';
import Noco from '../../../Noco'; import Noco from '../../../Noco';
import { genJwt } from './helpers'; import { genJwt } from './helpers';
import { randomTokenString } from '../../helpers/stringHelpers';
export async function signup(req: Request, res: Response<TableType>) { export async function signup(req: Request, res: Response<TableType>) {
const { const {
@ -31,6 +31,12 @@ export async function signup(req: Request, res: Response<TableType>) {
} = req.body; } = req.body;
let { password } = 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)) { if (!isEmail(_email)) {
NcError.badRequest(`Invalid email`); NcError.badRequest(`Invalid email`);
} }
@ -173,15 +179,14 @@ async function successfulSignIn({
await promisify((req as any).login.bind(req))(user); await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
let token_version = user.token_version; if (!user.token_version) {
if (!token_version) { user.token_version = randomTokenString();
token_version = randomTokenString();
} }
await User.update(user.id, { await User.update(user.id, {
refresh_token: refreshToken, refresh_token: refreshToken,
email: user.email, email: user.email,
token_version token_version: user.token_version
}); });
setTokenCookie(res, refreshToken); setTokenCookie(res, refreshToken);
@ -237,10 +242,6 @@ async function googleSignin(req, res, next) {
)(req, res, next); )(req, res, next);
} }
function randomTokenString(): string {
return crypto.randomBytes(40).toString('hex');
}
function setTokenCookie(res, token): void { function setTokenCookie(res, token): void {
// create http only cookie with refresh token that expires in 7 days // create http only cookie with refresh token that expires in 7 days
const cookieOptions = { const cookieOptions = {
@ -262,6 +263,13 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {
return NcError.badRequest('Missing new/old password'); 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 user = await User.getByEmail((req as any).user.email);
const hashedPassword = await promisify(bcrypt.hash)( const hashedPassword = await promisify(bcrypt.hash)(
currentPassword, currentPassword,
@ -318,10 +326,10 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
subject: 'Password Reset Link', subject: 'Password Reset Link',
text: `Visit following link to update your password : ${ text: `Visit following link to update your password : ${
(req as any).ncSiteUrl (req as any).ncSiteUrl
}/api/v1/db/auth/password/reset/${token}.`, }/api/v1/auth/password/reset/${token}.`,
html: ejs.render(template, { html: ejs.render(template, {
resetLink: 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'); 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 salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(req.body.password, salt); const password = await promisify(bcrypt.hash)(req.body.password, salt);
@ -501,7 +515,7 @@ const mapRoutes = router => {
})(req, res, next) })(req, res, next)
); );
// new API // deprecated APIs
router.post('/api/v1/db/auth/user/signup', catchError(signup)); router.post('/api/v1/db/auth/user/signup', catchError(signup));
router.post('/api/v1/db/auth/user/signin', catchError(signin)); router.post('/api/v1/db/auth/user/signin', catchError(signin));
router.get( router.get(
@ -534,5 +548,39 @@ const mapRoutes = router => {
'/api/v1/db/auth/password/reset/:tokenId', '/api/v1/db/auth/password/reset/:tokenId',
catchError(renderPasswordReset) 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 }; 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); 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; const { apiMeta } = req.body;
if (apiMeta?.body) { if (apiMeta?.body) {
try { try {
apiMeta.body = JSON.parse(apiMeta.body); apiMeta.body = JSON.parse(apiMeta.body);
@ -106,12 +115,32 @@ export async function axiosRequestMake(req: Request, res: Response) {
return res.json(data?.data); 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 => { export default router => {
router.post( router.post(
'/api/v1/db/meta/connection/test', '/api/v1/db/meta/connection/test',
ncMetaAclMw(testConnection, 'testConnection') ncMetaAclMw(testConnection, 'testConnection')
); );
router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo)); 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.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 Local from '../../v1-legacy/plugins/adapters/storage/Local';
import { MetaTable } from '../../utils/globals'; import { MetaTable } from '../../utils/globals';
import { PluginCategory } from 'nocodb-sdk'; import { PluginCategory } from 'nocodb-sdk';
import Plugin from '../../models/Plugin';
const defaultPlugins = [ const defaultPlugins = [
SlackPluginConfig, SlackPluginConfig,
@ -97,25 +98,54 @@ class NcPluginMgrv2 {
pluginConfig.id pluginConfig.id
); );
} }
}
await this.initPluginsFromEnv();
}
/* init only the active plugins */ private static async initPluginsFromEnv() {
// if (pluginConfig?.active) { /*
// const tempPlugin = new plugin.builder(this.app, plugin); * NC_S3_BUCKET_NAME
// * NC_S3_REGION
// this.activePlugins.push(tempPlugin); * NC_S3_ACCESS_KEY
// * NC_S3_ACCESS_SECRET
// if (pluginConfig?.input) { * */
// pluginConfig.input = JSON.parse(pluginConfig.input);
// } if (
// process.env.NC_S3_BUCKET_NAME &&
// try { process.env.NC_S3_REGION &&
// await tempPlugin.init(pluginConfig?.input); process.env.NC_S3_ACCESS_KEY &&
// } catch (e) { process.env.NC_S3_ACCESS_SECRET
// console.log( ) {
// `Plugin(${plugin?.title}) initialization failed : ${e.message}` 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?: comparison_op?:
| 'eq' | 'eq'
| 'neq' | 'neq'
| 'not'
| 'like' | 'like'
| 'nlike' | 'nlike'
| 'empty' | 'empty'

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

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

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

@ -91,7 +91,7 @@ export default class Plugin implements PluginType {
/** /**
* get plugin by title * get plugin by title
*/ */
public static async getPluginByTitle(title: string) { public static async getPluginByTitle(title: string, ncMeta = Noco.ncMeta) {
let plugin = let plugin =
title && title &&
(await NocoCache.get( (await NocoCache.get(
@ -99,7 +99,7 @@ export default class Plugin implements PluginType {
CacheGetType.TYPE_OBJECT CacheGetType.TYPE_OBJECT
)); ));
if (!plugin) { if (!plugin) {
plugin = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { plugin = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
title title
}); });
await NocoCache.set(`${CacheScope.PLUGIN}:${title}`, plugin); 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) { if (updateObj.email) {
updateObj.email = updateObj.email.toLowerCase(); 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 // get existing cache
const keys = [ 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); 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) { public static async list(modelId: string, ncMeta = Noco.ncMeta) {
let viewsList = await NocoCache.getList(CacheScope.VIEW, [modelId]); let viewsList = await NocoCache.getList(CacheScope.VIEW, [modelId]);
if (!viewsList.length) { if (!viewsList.length) {
@ -666,6 +692,9 @@ export default class View implements ViewType {
if (o) { if (o) {
// update data // update data
o = { ...o, ...updateObj }; o = { ...o, ...updateObj };
if (o.is_default) {
await NocoCache.set(`${CacheScope.VIEW}:${o.fk_model_id}:default`, o);
}
// set cache // set cache
await NocoCache.set(key, o); 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-restTableOps.js",
"test/pg-restViews.js", "test/pg-restViews.js",
"test/pg-restRoles.js", "test/pg-restRoles.js",
"test/pg-restMisc.js" "test/pg-restMisc.js",
"common/9a_QuickTest.js"
], ],
"defaultCommandTimeout": 13000, "defaultCommandTimeout": 13000,
"pageLoadTimeout": 600000, "pageLoadTimeout": 600000,
@ -47,7 +48,7 @@
"screenshot": false, "screenshot": false,
"airtable": { "airtable": {
"apiKey": "keyn1MR87qgyUsYg4", "apiKey": "keyn1MR87qgyUsYg4",
"sharedBase": "https://airtable.com/shrkSQdtKNzUfAbIY" "sharedBase": "https://airtable.com/shr4z0qmh6dg5s3eB"
} }
}, },
"fixturesFolder": "scripts/cypress/fixtures", "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.

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

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

2
scripts/cypress/integration/common/1d_pg_table_view_drag_drop_reorder.js

@ -23,7 +23,7 @@ export const genTest = (apiType, dbType) => {
/* /*
Original order of list items Original order of list items
Actor, Address, Category, City, Country, Customer, FIlm, FilmText, Language, Payment, Rental Staff Actor, Address, Category, City, Country, Customer, FIlm, FilmText, Language, Payment, Rental Staff
ActorInfo, CustomerList, FilmList, NiceButSlowerFilmList, SalesByFilmCategory, SalesByStore, StaffList ActorInfo, Customer List, Film List, NiceButSlowerFilm List, SalesByFilmCategory, SalesByStore, Staff List
*/ */
it(`Table & SQL View list, Drag/drop`, () => { it(`Table & SQL View list, Drag/drop`, () => {

2
scripts/cypress/integration/common/1d_table_view_drag_drop_reorder.js

@ -18,7 +18,7 @@ export const genTest = (apiType, dbType) => {
/* /*
Original order of list items Original order of list items
Actor, Address, Category, City, Country, Customer, FIlm, FilmText, Language, Payment, Rental Staff Actor, Address, Category, City, Country, Customer, FIlm, FilmText, Language, Payment, Rental Staff
ActorInfo, CustomerList, FilmList, NiceButSlowerFilmList, SalesByFilmCategory, SalesByStore, StaffList ActorInfo, Customer List, Film List, NiceButSlowerFilm List, SalesByFilmCategory, SalesByStore, Staff List
*/ */
before(() => { before(() => {

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

@ -23,12 +23,12 @@ export const genTest = (apiType, dbType) => {
it("Expand belongs-to column", () => { it("Expand belongs-to column", () => {
// expand first row // expand first row
cy.get('td[data-col="CityList"] div:visible', { cy.get('td[data-col="City List"] div:visible', {
timeout: 12000, timeout: 12000,
}) })
.first() .first()
.click(); .click();
cy.get('td[data-col="CityList"] div .mdi-arrow-expand:visible') cy.get('td[data-col="City List"] div .mdi-arrow-expand:visible')
.first() .first()
.click(); .click();

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

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

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

Loading…
Cancel
Save