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

266
.github/workflows/release-executables.yml

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

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

@ -37,7 +37,7 @@ jobs:
outputs:
nightly_build_tag: ${{ steps.tag-step.outputs.NIGHTLY_BUILD_TAG }}
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
release-npm:
needs: set-tag
@ -48,6 +48,15 @@ jobs:
secrets:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Build docker image and push to docker hub
release-docker:
needs: [set-tag, release-npm]

15
.github/workflows/release-nocodb.yml

@ -44,14 +44,14 @@ jobs:
# bump the version from PREV_TAG
TARGET_TAG=$(echo ${PREV_TAG} | awk -F. -v OFS=. '{$NF += 1 ; print}')
fi
echo target version: ${TARGET_TAG}
echo previous version: ${PREV_TAG}
echo "::set-output name=target_tag::${TARGET_TAG}"
echo "::set-output name=prev_tag::${PREV_TAG}"
- name: Verify
run : |
echo ${{ steps.process-input.outputs.target_tag }}
echo ${{ steps.process-input.outputs.target_tag }}
# Merge develop to master
pr-to-master:
@ -94,6 +94,15 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [release-draft-note, process-input]
uses: ./.github/workflows/release-executables.yml
with:
tag: ${{ needs.process-input.outputs.target_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Close all issues with target tags 'Fixed' & 'Resolved'
close-fixed-issues:
needs: [release-docker, process-input]
@ -101,7 +110,7 @@ jobs:
with:
issue_label: 'Status: Fixed'
version: ${{ needs.process-input.outputs.target_tag }}
close-resolved-issues:
needs: [close-fixed-issues, process-input]
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 }}
outputs:
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
release-npm:
@ -77,13 +77,22 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# leave-comment:
# runs-on: 'ubuntu-latest'
# needs: [release-docker, set-tag]
# steps:
# - run: |
# echo docker run -d -p 8888:8080 nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
leave-comment:
if: ${{ github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false }}
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 }}
```
leave-executable-comment:
if: ${{ github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false }}
runs-on: 'ubuntu-latest'
needs: [release-executables, set-tag]
steps:
- uses: peter-evans/commit-comment@v2
with:
body: |
### Run Executables
#### MacOS
```bash
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} && cd "$_" \
&& curl http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-macos-arm64 -o noco -L \
&& chmod +x noco \
&& ./noco
```
#### Linux
```bash
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} && cd "$_" \
&& curl http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-linux-x64 -o noco -L \
&& chmod +x noco \
&& ./noco
```
#### Windows
```bash
iwp http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-win-arm64.exe
.\Noco-win-arm64.exe
```
For executables visit [here](https://github.com/nocodb/nocodb-timely/releases/tag/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }})
# if-merged:
# if: github.event.pull_request.merged == true
# runs-on: ubuntu-latest

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

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

11
package.json

@ -15,11 +15,12 @@
"scripts": {
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build",
"install:common": "cd ./packages/nocodb; npm install ../nocodb-sdk; cd ../nc-gui; npm install ../nocodb-sdk",
"start:api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "cd ./packages/nc-gui; npm install; npm run dev",
"start:api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:xcdb-api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "npm run build:common ; cd ./packages/nc-gui; npm install ../nocodb-sdk; npm install; npm run dev",
"cypress:run": "cypress run --config-file ./scripts/cypress/cypress.json",
"cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json",
"cypress:clear": "cypress cache clear",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,69 @@
<template>
<input
v-model="localValue"
:placeholder="durationPlaceholder"
readonly
>
</template>
<script>
import { durationOptions, convertMS2Duration } from '~/helpers/durationHelper'
export default {
name: 'DurationCell',
props: {
column: Object,
value: [String, Number]
},
data: () => ({
showWarningMessage: false,
localValue: null
}),
computed: {
durationPlaceholder() {
return durationOptions[this.column?.meta?.duration || 0].title
}
},
watch: {
'column.meta.duration'(newValue, oldValue) {
if (oldValue !== newValue) {
this.localValue = convertMS2Duration(this.value, newValue)
}
},
value(val, oldVal) {
this.localValue = convertMS2Duration(val !== oldVal && (!val && val !== 0) ? oldVal : val, this.column?.meta?.duration || 0)
}
},
created() {
this.localValue = convertMS2Duration(this.value, this.column?.meta?.duration || 0)
}
}
</script>
<style scoped>
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

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

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

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

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

@ -0,0 +1,139 @@
<template>
<div class="duration-cell-wrapper">
<input
ref="durationInput"
v-model="localState"
:placeholder="durationPlaceholder"
@blur="onBlur"
@keypress="checkDurationFormat($event)"
@keydown.enter="isEdited && $emit('input', durationInMS)"
v-on="parentListeners"
>
<div v-if="showWarningMessage == true" class="duration-warning">
<!-- TODO: i18n -->
Please enter a number
</div>
</div>
</template>
<script>
import { durationOptions, convertMS2Duration, convertDurationToSeconds } from '~/helpers/durationHelper'
export default {
name: 'DurationCell',
props: {
column: Object,
value: [Number, String],
readOnly: Boolean
},
data: () => ({
// flag to determine to show warning message or not
showWarningMessage: false,
// duration in milliseconds
durationInMS: null,
// check if the cell is edited or not
isEdited: false
}),
computed: {
localState: {
get() {
return convertMS2Duration(this.value, this.durationType)
},
set(val) {
this.isEdited = true
const res = convertDurationToSeconds(val, this.durationType)
if (res._isValid) {
this.durationInMS = res._sec
}
}
},
durationPlaceholder() {
return durationOptions[this.durationType].title
},
durationType() {
return this.column?.meta?.duration || 0
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
mounted() {
window.addEventListener('keypress', (_) => {
if (this.$refs.durationInput) {
this.$refs.durationInput.focus()
}
})
},
methods: {
checkDurationFormat(evt) {
evt = evt || window.event
const charCode = (evt.which) ? evt.which : evt.keyCode
// ref: http://www.columbia.edu/kermit/ascii.html
const PRINTABLE_CTL_RANGE = charCode > 31
const NON_DIGIT = charCode < 48 || charCode > 57
const NON_COLON = charCode !== 58
const NON_PERIOD = charCode !== 46
if (PRINTABLE_CTL_RANGE && NON_DIGIT && NON_COLON && NON_PERIOD) {
this.showWarningMessage = true
evt.preventDefault()
} else {
this.showWarningMessage = false
// only allow digits, '.' and ':' (without quotes)
return true
}
},
onBlur() {
if (this.isEdited) {
this.$emit('input', this.durationInMS)
}
this.isEdited = false
}
}
}
</script>
<style scoped>
.duration-cell-wrapper {
padding: 10px;
}
.duration-warning {
text-align: left;
margin-top: 10px;
color: #E65100;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

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

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

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

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

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

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

@ -0,0 +1,192 @@
export const durationOptions = [
{
id: 0,
title: 'h:mm',
example: '(e.g. 1:23)',
regex: /(\d+)(?::(\d+))?/
}, {
id: 1,
title: 'h:mm:ss',
example: '(e.g. 3:45, 1:23:40)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?/
}, {
id: 2,
title: 'h:mm:ss.s',
example: '(e.g. 3:34.6, 1:23:40.0)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}, {
id: 3,
title: 'h:mm:ss.ss',
example: '(e.g. 3.45.67, 1:23:40.00)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}, {
id: 4,
title: 'h:mm:ss.sss',
example: '(e.g. 3.45.678, 1:23:40.000)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}
]
// pad zero
// mm && ss
// e.g. 3 -> 03
// e.g. 12 -> 12
// sss
// e.g. 1 -> 001
// e.g. 10 -> 010
const padZero = (val, isSSS = false) => {
return (val + '').padStart(isSSS ? 3 : 2, '0')
}
export const convertMS2Duration = (val, durationType) => {
if (val === "" || val === null || val === undefined) { return val }
// 600.000 s --> 10:00 (10 mins)
const milliseconds = Math.round((val % 1) * 1000)
const centiseconds = Math.round(milliseconds / 10)
const deciseconds = Math.round(centiseconds / 10)
const hours = Math.floor(parseInt(val, 10) / (60 * 60))
const minutes = Math.floor((parseInt(val, 10) - (hours * 60 * 60)) / 60)
const seconds = parseInt(val, 10) - (hours * 60 * 60) - (minutes * 60)
if (durationType === 0) {
// h:mm
return `${padZero(hours)}:${padZero(minutes + (seconds >= 30))}`
} else if (durationType === 1) {
// h:mm:ss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`
} else if (durationType === 2) {
// h:mm:ss.s
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${deciseconds}`
} else if (durationType === 3) {
// h:mm:ss.ss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${padZero(centiseconds)}`
} else if (durationType === 4) {
// h:mm:ss.sss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${padZero(milliseconds, true)}`
}
return val
}
export const convertDurationToSeconds = (val, durationType) => {
// 10:00 (10 mins) -> 600.000 s
const res = {
_ms: null,
_isValid: true
}
const durationRegex = durationOptions[durationType].regex
if (durationRegex.test(val)) {
let h, mm, ss
const groups = val.match(durationRegex)
if (groups[0] && groups[1] && !groups[2] && !groups[3] && !groups[4]) {
const val = parseInt(groups[1], 10)
if (groups.input.slice(-1) === ':') {
// e.g. 30:
h = groups[1]
mm = 0
ss = 0
} else if (durationType === 0) {
// consider it as minutes
// e.g. 360 -> 06:00
h = Math.floor(val / 60)
mm = Math.floor((val - ((h * 3600)) / 60))
ss = 0
} else {
// consider it as seconds
// e.g. 3600 -> 01:00:00
h = Math.floor(groups[1] / 3600)
mm = Math.floor(groups[1] / 60) % 60
ss = val % 60
}
} else if (durationType !== 0 && groups[1] && groups[2] && !groups[3]) {
// 10:10 means mm:ss instead of h:mm
// 10:10:10 means h:mm:ss
h = 0
mm = groups[1]
ss = groups[2]
} else {
h = groups[1] || 0
mm = groups[2] || 0
ss = groups[3] || 0
}
if (durationType === 0) {
// h:mm
res._sec = h * 3600 + mm * 60
} else if (durationType === 1) {
// h:mm:ss
res._sec = h * 3600 + mm * 60 + ss * 1
} else if (durationType === 2) {
// h:mm:ss.s (deciseconds)
const ds = groups[4] || 0
const len = Math.log(ds) * Math.LOG10E + 1 | 0
const ms = (
// e.g. len = 4: 1234 -> 1, 1456 -> 1
// e.g. len = 3: 123 -> 1, 191 -> 2
// e.g. len = 2: 12 -> 1 , 16 -> 2
len === 4
? Math.round(ds / 1000)
: len === 3
? Math.round(ds / 100)
: len === 2
? Math.round(ds / 10)
// take whatever it is
: ds
) * 100
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
} else if (durationType === 3) {
// h:mm:ss.ss (centiseconds)
const cs = groups[4] || 0
const len = Math.log(cs) * Math.LOG10E + 1 | 0
const ms = (
// e.g. len = 4: 1234 -> 12, 1285 -> 13
// e.g. len = 3: 123 -> 12, 128 -> 13
// check the third digit
len === 4
? Math.round(cs / 100)
: len === 3
? Math.round(cs / 10)
// take whatever it is
: cs
) * 10
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
} else if (durationType === 4) {
// h:mm:ss.sss (milliseconds)
let ms = groups[4] || 0
const len = Math.log(ms) * Math.LOG10E + 1 | 0
ms = (
// e.g. 1235 -> 124
// e.g. 1234 -> 123
len === 4
? Math.round(ms / 10)
// take whatever it is
: ms
) * 1
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
}
} else {
res._isValid = false
}
return res
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

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

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

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

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

2
packages/nc-gui/package.json

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.
<alert>
<alert id="password-conditions">
Your password has at least 8 letters with one uppercase, one number and one special letter
</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.
![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"
description: "Primary value"
position: 575
position: 580
category: "Product"
menuTitle: "Primary value"
---

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5
packages/nocodb/README.md

@ -1,10 +1,11 @@
# nocodb
# Nocodb
## Running locally
Even though this package is a backend project, you can still visit the dashboard as it includes ``nc-lib-gui``.
```
npm install
npm run watch:run
# open localhost:8080/dashboard in browser
@ -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"
```
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",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.61",
"nc-help": "0.2.67",
"nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "0.91.10",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"ora": "^4.0.4",
@ -232,7 +232,6 @@
},
"../nocodb-sdk": {
"version": "0.91.10",
"extraneous": true,
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
@ -16353,9 +16352,9 @@
}
},
"node_modules/nc-help": {
"version": "0.2.61",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.61.tgz",
"integrity": "sha512-YvFD0EjhwLs36U4GqIg0uxYWBurc39Wt7Hi7pd4/Lh1A/hmpWl6eZD4k9gk2FqqtGMlR/7e5UOmFYrRuvtzkTw==",
"version": "0.2.67",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.67.tgz",
"integrity": "sha512-O9eXHrpO0dBdFv6zUZAos+63JZEGhZ2lG+MduGZ+/BL7M5b0qU7d9b95Pmgq6Gd5wO3txT/7x7uPBHZxeSgvHQ==",
"dependencies": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",
@ -16571,16 +16570,8 @@
"dev": true
},
"node_modules/nocodb-sdk": {
"version": "0.91.10",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"engines": {
"node": ">=10"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-addon-api": {
"version": "2.0.0",
@ -38049,9 +38040,9 @@
"integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw=="
},
"nc-help": {
"version": "0.2.61",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.61.tgz",
"integrity": "sha512-YvFD0EjhwLs36U4GqIg0uxYWBurc39Wt7Hi7pd4/Lh1A/hmpWl6eZD4k9gk2FqqtGMlR/7e5UOmFYrRuvtzkTw==",
"version": "0.2.67",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.67.tgz",
"integrity": "sha512-O9eXHrpO0dBdFv6zUZAos+63JZEGhZ2lG+MduGZ+/BL7M5b0qU7d9b95Pmgq6Gd5wO3txT/7x7uPBHZxeSgvHQ==",
"requires": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",
@ -38231,12 +38222,33 @@
"dev": true
},
"nocodb-sdk": {
"version": "0.91.10",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.91.10.tgz",
"integrity": "sha512-mdddLp+HgpPJwrKwmZrOPvjHaEbKbjdUP8vkJ9icQPXTJizclPBLugKhvp2hPlNPfJ3BIdfAj38RKBYlFu+now==",
"version": "file:../nocodb-sdk",
"requires": {
"@ava/typescript": "^1.1.1",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^3.12.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"gh-pages": "^3.1.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^6.0.1",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.0",
"typescript": "^4.0.2"
}
},
"node-addon-api": {

7
packages/nocodb/package.json

@ -69,6 +69,7 @@
"watch:serve": "nodemon -e ts -w ./build -x npm run debug-local ",
"watch:run": "cross-env NC_DISABLE_TELE1=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:cypress": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:cypress:pg": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"watch:run:mysql": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunMysql --log-error --project tsconfig.json\"",
"watch:run:pg": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"run": "ts-node src/run/docker",
@ -153,11 +154,11 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.61",
"nc-help": "0.2.67",
"nc-lib-gui": "0.91.10",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "0.91.10",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"ora": "^4.0.4",
@ -257,4 +258,4 @@
"**/*.spec.js"
]
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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;
}
if (projectBody?.title.length > 50) {
NcError.badRequest('Project title exceeds 50 characters');
}
if (await Project.getByTitle(projectBody?.title)) {
NcError.badRequest('Project title already in use');
}
@ -211,7 +215,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
hm,
title: `${hm.title}List`
title: `${hm.title} List`
};
}),
...belongsTo.map(bt => {
@ -226,7 +230,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}Read`
title: `${bt.rtitle}`
};
})
];
@ -412,6 +416,13 @@ export async function projectCost(req, res) {
cost = Math.min(120 * userCount, 36000);
}
Tele.event({
event: 'a:project:cost',
data: {
cost
}
});
res.json({ cost });
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -60,8 +60,17 @@ export async function releaseVersion(_req: Request, res: Response) {
res.json(result);
}
export async function axiosRequestMake(req: Request, res: Response) {
export async function appHealth(_: Request, res: Response) {
res.json({
message: 'OK',
timestamp: Date.now(),
uptime: process.uptime()
});
}
async function _axiosRequestMake(req: Request, res: Response) {
const { apiMeta } = req.body;
if (apiMeta?.body) {
try {
apiMeta.body = JSON.parse(apiMeta.body);
@ -106,12 +115,32 @@ export async function axiosRequestMake(req: Request, res: Response) {
return res.json(data?.data);
}
export async function axiosRequestMake(req: Request, res: Response) {
const {
apiMeta: { url }
} = req.body;
const isExcelImport = /.*\.(xls|xlsx|xlsm|ods|ots)/;
const isCSVImport = /.*\.(csv)/;
const ipBlockList = /(10)(\.([2]([0-5][0-5]|[01234][6-9])|[1][0-9][0-9]|[1-9][0-9]|[0-9])){3}|(172)\.(1[6-9]|2[0-9]|3[0-1])(\.(2[0-4][0-9]|25[0-5]|[1][0-9][0-9]|[1-9][0-9]|[0-9])){2}|(192)\.(168)(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){2}|(0.0.0.0)|localhost?/g;
if (
ipBlockList.test(url) ||
(!isCSVImport.test(url) && !isExcelImport.test(url))
) {
return res.json({});
}
if (isCSVImport || isExcelImport) {
req.body.apiMeta.responseType = 'arraybuffer';
}
return await _axiosRequestMake(req, res);
}
export default router => {
router.post(
'/api/v1/db/meta/connection/test',
ncMetaAclMw(testConnection, 'testConnection')
);
router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo));
router.get('/api/v1/db/meta/nocodb/version', catchError(releaseVersion));
router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake));
router.get('/api/v1/version', catchError(releaseVersion));
router.get('/api/v1/health', catchError(appHealth));
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

5
scripts/cypress/cypress.json

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

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

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

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

Binary file not shown.

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

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

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

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", () => {
// expand first row
cy.get('td[data-col="CityList"] div:visible', {
cy.get('td[data-col="City List"] div:visible', {
timeout: 12000,
})
.first()
.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()
.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", () => {
// expand first row
cy.get('td[data-col="FilmMMList"] div', { timeout: 12000 })
cy.get('td[data-col="Film List"] div', { timeout: 12000 })
.first()
.click({ force: true });
cy.get('td[data-col="FilmMMList"] div .mdi-arrow-expand')
cy.get('td[data-col="Film List"] div .mdi-arrow-expand')
.first()
.click({ force: true });

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

Loading…
Cancel
Save