Browse Source

Merge branch 'develop' into fix/formula-empty-result

pull/4644/head
Wing-Kam Wong 2 years ago
parent
commit
8d27065f47
  1. 32
      .github/uffizzi/docker-compose.uffizzi.yml
  2. 8
      .github/workflows/release-docker.yml
  3. 5
      packages/nc-gui/components/general/ReleaseInfo.vue
  4. 100
      packages/nc-gui/lang/ru.json
  5. 3
      packages/nc-gui/pages/[projectType]/[projectId]/index/index/index.vue
  6. 18
      packages/noco-docs/content/en/engineering/builds-and-releases.md
  7. 11
      packages/nocodb/package-lock.json
  8. 1
      packages/nocodb/package.json
  9. 45
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
  10. 29
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts
  11. 2
      packages/nocodb/src/lib/meta/api/tableApis.ts
  12. 22
      packages/nocodb/src/lib/meta/api/utilApis.ts

32
.github/uffizzi/docker-compose.uffizzi.yml

@ -8,17 +8,45 @@ x-uffizzi:
services: services:
postgres: postgres:
image: postgres image: postgres
restart: always
environment: environment:
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_DB: root_db POSTGRES_DB: root_db
deploy:
resources:
limits:
memory: 500M
mssql:
image: "mcr.microsoft.com/mssql/server:2017-latest"
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: Password123.
deploy:
resources:
limits:
memory: 1000M
mysql:
environment:
MYSQL_DATABASE: root_db
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: noco
image: "mysql:5.7"
deploy:
resources:
limits:
memory: 500M
nocodb: nocodb:
image: "${NOCODB_IMAGE}" image: "${NOCODB_IMAGE}"
ports: ports:
- "8080:8080" - "8080:8080"
restart: always entrypoint: /bin/sh
command: ["-c", "apk add wait4ports && wait4ports tcp://localhost:5432 && /usr/src/appEntry/start.sh"]
environment: environment:
NC_DB: "pg://localhost:5432?u=postgres&p=password&d=root_db" NC_DB: "pg://localhost:5432?u=postgres&p=password&d=root_db"
NC_ADMIN_EMAIL: admin@nocodb.com NC_ADMIN_EMAIL: admin@nocodb.com
NC_ADMIN_PASSWORD: password NC_ADMIN_PASSWORD: password
deploy:
resources:
limits:
memory: 500M

8
.github/workflows/release-docker.yml

@ -50,6 +50,10 @@ jobs:
run: | run: |
DOCKER_REPOSITORY=nocodb DOCKER_REPOSITORY=nocodb
DOCKER_BUILD_TAG=${{ github.event.inputs.tag || inputs.tag }} DOCKER_BUILD_TAG=${{ github.event.inputs.tag || inputs.tag }}
DOCKER_BUILD_LATEST_TAG=latest
if [[ "$DOCKER_BUILD_TAG" =~ "-beta." ]]; then
DOCKER_BUILD_LATEST_TAG=$(echo $DOCKER_BUILD_TAG | awk -F '-beta.' '{print $1}')-beta.latest
fi
if [[ ${{ github.event.inputs.targetEnv || inputs.targetEnv }} == 'DEV' ]]; then if [[ ${{ github.event.inputs.targetEnv || inputs.targetEnv }} == 'DEV' ]]; then
if [[ ${{ github.event.inputs.currentVersion || inputs.currentVersion || 'N/A' }} != 'N/A' ]]; then if [[ ${{ github.event.inputs.currentVersion || inputs.currentVersion || 'N/A' }} != 'N/A' ]]; then
DOCKER_BUILD_TAG=${{ github.event.inputs.currentVersion || inputs.currentVersion }}-${{ github.event.inputs.tag || inputs.tag }} DOCKER_BUILD_TAG=${{ github.event.inputs.currentVersion || inputs.currentVersion }}-${{ github.event.inputs.tag || inputs.tag }}
@ -62,8 +66,10 @@ jobs:
fi fi
echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_OUTPUT echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_OUTPUT
echo "DOCKER_BUILD_TAG=${DOCKER_BUILD_TAG}" >> $GITHUB_OUTPUT echo "DOCKER_BUILD_TAG=${DOCKER_BUILD_TAG}" >> $GITHUB_OUTPUT
echo "DOCKER_BUILD_LATEST_TAG=${DOCKER_BUILD_LATEST_TAG}" >> $GITHUB_OUTPUT
echo DOCKER_REPOSITORY: ${DOCKER_REPOSITORY} echo DOCKER_REPOSITORY: ${DOCKER_REPOSITORY}
echo DOCKER_BUILD_TAG: ${DOCKER_BUILD_TAG} echo DOCKER_BUILD_TAG: ${DOCKER_BUILD_TAG}
echo DOCKER_BUILD_LATEST_TAG: ${DOCKER_BUILD_LATEST_TAG}
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -134,7 +140,7 @@ jobs:
push: true push: true
tags: | tags: |
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }} nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }}
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:latest nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_LATEST_TAG }}
# Temp fix # Temp fix
# https://github.com/docker/build-push-action/issues/252 # https://github.com/docker/build-push-action/issues/252

5
packages/nc-gui/components/general/ReleaseInfo.vue

@ -7,6 +7,9 @@ const { currentVersion, latestRelease, hiddenRelease } = useGlobal()
const releaseAlert = computed({ const releaseAlert = computed({
get() { get() {
if (currentVersion.value?.includes('-beta.') || latestRelease.value?.includes('-beta.')) {
return false
}
return ( return (
currentVersion.value && currentVersion.value &&
latestRelease.value && latestRelease.value &&
@ -22,7 +25,7 @@ const releaseAlert = computed({
async function fetchReleaseInfo() { async function fetchReleaseInfo() {
try { try {
const versionInfo = await $api.utils.appVersion() const versionInfo = await $api.utils.appVersion()
if (versionInfo && versionInfo.releaseVersion && versionInfo.currentVersion && !/[^0-9.]/.test(versionInfo.currentVersion)) { if (versionInfo && versionInfo.releaseVersion && versionInfo.currentVersion) {
currentVersion.value = versionInfo.currentVersion currentVersion.value = versionInfo.currentVersion
latestRelease.value = versionInfo.releaseVersion latestRelease.value = versionInfo.releaseVersion
} else { } else {

100
packages/nc-gui/lang/ru.json

@ -284,17 +284,17 @@
"requestDataSource": "Request a data source you need?", "requestDataSource": "Request a data source you need?",
"apiKey": "API Key", "apiKey": "API Key",
"sharedBase": "Shared Base", "sharedBase": "Shared Base",
"importData": "Import Data", "importData": "Импорт данных",
"importSecondaryViews": "Import Secondary Views", "importSecondaryViews": "Import Secondary Views",
"importRollupColumns": "Import Rollup Columns", "importRollupColumns": "Import Rollup Columns",
"importLookupColumns": "Import Lookup Columns", "importLookupColumns": "Import Lookup Columns",
"importAttachmentColumns": "Import Attachment Columns", "importAttachmentColumns": "Import Attachment Columns",
"importFormulaColumns": "Import Formula Columns", "importFormulaColumns": "Import Formula Columns",
"noData": "No Data", "noData": "Нет данных",
"goToDashboard": "Go to Dashboard", "goToDashboard": "Перейти к панели управления",
"importing": "Importing", "importing": "Импорт",
"flattenNested": "Flatten Nested", "flattenNested": "Flatten Nested",
"downloadAllowed": "Download allowed", "downloadAllowed": "Скачивание разрешено",
"weAreHiring": "We are Hiring!", "weAreHiring": "We are Hiring!",
"primaryKey": "Primary key", "primaryKey": "Primary key",
"hasMany": "has many", "hasMany": "has many",
@ -302,13 +302,13 @@
"manyToMany": "have many to many relation", "manyToMany": "have many to many relation",
"extraConnectionParameters": "Extra connection parameters", "extraConnectionParameters": "Extra connection parameters",
"commentsOnly": "Comments only", "commentsOnly": "Comments only",
"documentation": "Documentation", "documentation": "Документация",
"subscribeNewsletter": "Subscribe to our weekly newsletter", "subscribeNewsletter": "Подпишитесь на нашу еженедельную рассылку",
"signUpWithGoogle": "Sign up with Google", "signUpWithGoogle": "Зарегистрируйтесь с помощью Google",
"signInWithGoogle": "Sign in with Google", "signInWithGoogle": "Войти при помощи Google",
"agreeToTos": "By signing up, you agree to the Terms of Service", "agreeToTos": "By signing up, you agree to the Terms of Service",
"welcomeToNc": "Welcome to NocoDB!", "welcomeToNc": "Добро пожаловать в NocoDB!",
"inviteOnlySignup": "Allow signup only using invite url" "inviteOnlySignup": "Разрешить регистрацию только по ссылке"
}, },
"activity": { "activity": {
"createProject": "Создать проект", "createProject": "Создать проект",
@ -353,14 +353,14 @@
"invite": "Пригласить", "invite": "Пригласить",
"inviteMore": "Пригласить еще", "inviteMore": "Пригласить еще",
"inviteTeam": "Пригласить команду", "inviteTeam": "Пригласить команду",
"inviteUser": "Invite User", "inviteUser": "Пригласить пользователя",
"inviteToken": "Токен приглашения", "inviteToken": "Токен приглашения",
"newUser": "Новый пользователь", "newUser": "Новый пользователь",
"editUser": "Редактировать пользователя", "editUser": "Редактировать пользователя",
"deleteUser": "Удалить пользователя из проекта", "deleteUser": "Удалить пользователя из проекта",
"resendInvite": "Переотправить приглашение e-mail", "resendInvite": "Переотправить приглашение e-mail",
"copyInviteURL": "Скопировать URL-адрес приглашения", "copyInviteURL": "Скопировать URL-адрес приглашения",
"copyPasswordResetURL": "Copy password reset URL", "copyPasswordResetURL": "Скопировать URL для сброса пароля",
"newRole": "Новая роль", "newRole": "Новая роль",
"reloadRoles": "Перезагрузить роли", "reloadRoles": "Перезагрузить роли",
"nextPage": "Следущая страница", "nextPage": "Следущая страница",
@ -376,13 +376,13 @@
"setPrimary": "Установить в качестве основного значения", "setPrimary": "Установить в качестве основного значения",
"addRow": "Добавить новую строку", "addRow": "Добавить новую строку",
"saveRow": "Сохранить строку", "saveRow": "Сохранить строку",
"saveAndExit": "Save & Exit", "saveAndExit": "Сохранить и выйти",
"saveAndStay": "Save & Stay", "saveAndStay": "Сохранить и остаться",
"insertRow": "Вставить новый строк", "insertRow": "Вставить новый строк",
"deleteRow": "Удалить строку", "deleteRow": "Удалить строку",
"deleteSelectedRow": "Удалить выбранные строки", "deleteSelectedRow": "Удалить выбранные строки",
"importExcel": "Импорт из Excel", "importExcel": "Импорт из Excel",
"importCSV": "Import CSV", "importCSV": "Импорт CSV",
"downloadCSV": "Скачать как CSV.", "downloadCSV": "Скачать как CSV.",
"downloadExcel": "Скачать как XLSX", "downloadExcel": "Скачать как XLSX",
"uploadCSV": "Загрузить CSV.", "uploadCSV": "Загрузить CSV.",
@ -421,12 +421,12 @@
"editConnJson": "Редактировать соединение JSON", "editConnJson": "Редактировать соединение JSON",
"sponsorUs": "Спонсируйте нас", "sponsorUs": "Спонсируйте нас",
"sendEmail": "Отправить письмо", "sendEmail": "Отправить письмо",
"addUserToProject": "Add user to project", "addUserToProject": "Добавить пользователя в проект",
"getApiSnippet": "Get API Snippet", "getApiSnippet": "Get API Snippet",
"clearCell": "Clear cell", "clearCell": "Очистить ячейку",
"addFilterGroup": "Add Filter Group", "addFilterGroup": "Добавить группу фильтров",
"linkRecord": "Link record", "linkRecord": "Link record",
"addNewRecord": "Add new record", "addNewRecord": "Добавить новую запись",
"useConnectionUrl": "Use Connection URL", "useConnectionUrl": "Use Connection URL",
"toggleCommentsDraw": "Toggle comments draw", "toggleCommentsDraw": "Toggle comments draw",
"expandRecord": "Развернуть запись", "expandRecord": "Развернуть запись",
@ -647,44 +647,44 @@
}, },
"invalidURL": "Неверный URL", "invalidURL": "Неверный URL",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Генератор шаблонов не найден!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Не удалось загрузить файл",
"primaryColumnUpdateFailed": "Failed to update primary column", "primaryColumnUpdateFailed": "Failed to update primary column",
"formDescriptionTooLong": "Data too long for Form Description", "formDescriptionTooLong": "Data too long for Form Description",
"columnsRequired": "Following columns are required", "columnsRequired": "Following columns are required",
"selectAtleastOneColumn": "At least one column has to be selected", "selectAtleastOneColumn": "Должен быть выбран как минимум один столбец",
"columnDescriptionNotFound": "Cannot find the destination column for", "columnDescriptionNotFound": "Cannot find the destination column for",
"duplicateMappingFound": "Duplicate mapping found, please remove one of the mapping", "duplicateMappingFound": "Duplicate mapping found, please remove one of the mapping",
"nullValueViolatesNotNull": "Null value violates not-null constraint", "nullValueViolatesNotNull": "Null value violates not-null constraint",
"sourceHasInvalidNumbers": "Source data contains some invalid numbers", "sourceHasInvalidNumbers": "Исходные данные содержат недопустимые числа",
"sourceHasInvalidBoolean": "Source data contains some invalid boolean values", "sourceHasInvalidBoolean": "Source data contains some invalid boolean values",
"invalidForm": "Invalid Form", "invalidForm": "Invalid Form",
"formValidationFailed": "Form validation failed", "formValidationFailed": "Ошибка проверки формы",
"youHaveBeenSignedOut": "You have been signed out", "youHaveBeenSignedOut": "Вы вышли из системы",
"failedToLoadList": "Failed to load list", "failedToLoadList": "Не удалось загрузить список",
"failedToLoadChildrenList": "Failed to load children list", "failedToLoadChildrenList": "Failed to load children list",
"deleteFailed": "Delete failed", "deleteFailed": "Не удалось удалить",
"unlinkFailed": "Unlink failed", "unlinkFailed": "Не удалось отменить связь",
"rowUpdateFailed": "Row update failed", "rowUpdateFailed": "Row update failed",
"deleteRowFailed": "Failed to delete row", "deleteRowFailed": "Не удалось удалить строку",
"setFormDataFailed": "Failed to set form data", "setFormDataFailed": "Не удалось задать данные формы",
"formViewUpdateFailed": "Failed to update form view", "formViewUpdateFailed": "Failed to update form view",
"tableNameRequired": "Table name is required", "tableNameRequired": "Требуется имя таблицы",
"nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _", "nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _",
"followingCharactersAreNotAllowed": "Following characters are not allowed", "followingCharactersAreNotAllowed": "Нельзя использовать следующие символы",
"columnNameRequired": "Column name is required", "columnNameRequired": "Требуется название столбца",
"projectNameExceeds50Characters": "Project name exceeds 50 characters", "projectNameExceeds50Characters": "Название проекта превышает 50 символов",
"projectNameCannotStartWithSpace": "Project name cannot start with space", "projectNameCannotStartWithSpace": "Название проекта не может начинаться с пробела",
"requiredField": "Required field", "requiredField": "Обязательное поле",
"ipNotAllowed": "IP not allowed", "ipNotAllowed": "IP not allowed",
"targetFileIsNotAnAcceptedFileType": "Target file is not an accepted file type", "targetFileIsNotAnAcceptedFileType": "Target file is not an accepted file type",
"theAcceptedFileTypeIsCsv": "The accepted file type is .csv", "theAcceptedFileTypeIsCsv": "Допустимый тип файла: .csv",
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots", "theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Допустимые типы файлов: .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.", "fieldRequired": "{value} не может быть пустым.",
"projectNotAccessible": "Project not accessible", "projectNotAccessible": "Проект недоступен",
"copyToClipboardError": "Failed to copy to clipboard" "copyToClipboardError": "Не удалось скопировать в буфер обмена"
}, },
"toast": { "toast": {
"exportMetadata": "Метаданные проекта успешно экспортированы", "exportMetadata": "Метаданные проекта успешно экспортированы",
@ -704,18 +704,18 @@
"futureRelease": "Скоро!" "futureRelease": "Скоро!"
}, },
"success": { "success": {
"columnDuplicated": "Column duplicated successfully", "columnDuplicated": "Столбец успешно скопирован",
"updatedUIACL": "Updated UI ACL for tables successfully", "updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully", "pluginUninstalled": "Плагин успешно удален",
"pluginSettingsSaved": "Plugin settings saved successfully", "pluginSettingsSaved": "Настройки плагина сохранены",
"pluginTested": "Successfully tested plugin settings", "pluginTested": "Successfully tested plugin settings",
"tableRenamed": "Table renamed successfully", "tableRenamed": "Таблица успешно переименована",
"viewDeleted": "View deleted successfully", "viewDeleted": "Представление успешно удалено",
"primaryColumnUpdated": "Successfully updated as primary column", "primaryColumnUpdated": "Successfully updated as primary column",
"tableDataExported": "Successfully exported all table data", "tableDataExported": "Все данные таблицы успешно экспортированы",
"updated": "Successfully updated", "updated": "Успешно обновлено",
"sharedViewDeleted": "Deleted shared view successfully", "sharedViewDeleted": "Deleted shared view successfully",
"userDeleted": "User deleted successfully", "userDeleted": "Пользователь успешно удален",
"viewRenamed": "View renamed successfully", "viewRenamed": "View renamed successfully",
"tokenGenerated": "Token generated successfully", "tokenGenerated": "Token generated successfully",
"tokenDeleted": "Token deleted successfully", "tokenDeleted": "Token deleted successfully",

3
packages/nc-gui/pages/[projectType]/[projectId]/index/index/index.vue

@ -19,7 +19,7 @@ const { isOverDropZone } = useDropZone(dropZone, onDrop)
const { files, open, reset } = useFileDialog() const { files, open, reset } = useFileDialog()
const { isSharedBase } = useProject() const { bases, isSharedBase } = useProject()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -128,6 +128,7 @@ function openCreateTable() {
const { close } = useDialog(resolveComponent('DlgTableCreate'), { const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen, 'modelValue': isOpen,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
'baseId': bases.value[0].id,
}) })
function closeDialog() { function closeDialog() {

18
packages/noco-docs/content/en/engineering/builds-and-releases.md

@ -6,7 +6,9 @@ category: "Engineering"
menuTitle: "Releases & Builds" menuTitle: "Releases & Builds"
--- ---
## Builds of NocoDB ## Builds of NocoDB
There are 3 kinds of docker builds in NocoDB There are 3 kinds of docker builds in NocoDB
- Release builds [nocodb/nocodb](https://hub.docker.com/r/nocodb/nocodb) : built during NocoDB release. - Release builds [nocodb/nocodb](https://hub.docker.com/r/nocodb/nocodb) : built during NocoDB release.
- Daily builds [nocodb/nocodb-daily](https://hub.docker.com/r/nocodb/nocodb-daily) : built every 6 hours from Develop branch. - Daily builds [nocodb/nocodb-daily](https://hub.docker.com/r/nocodb/nocodb-daily) : built every 6 hours from Develop branch.
- Daily builds [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely): built for every PR. - Daily builds [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely): built for every PR.
@ -26,12 +28,24 @@ Below is an overview of how to make these builds and what happens behind the sce
![image](https://user-images.githubusercontent.com/35857179/167240383-dda05f76-8323-4f4a-b3e7-9db886dbd68d.png) ![image](https://user-images.githubusercontent.com/35857179/167240383-dda05f76-8323-4f4a-b3e7-9db886dbd68d.png)
- Then there would be two cases - you can either leave target tag and pervious tag blank or manually input some values - Then there would be two cases - you can either leave target tag and pervious tag blank or manually input some values
> Target Tag means the target deployment version, while Previous Tag means the latest version as of now. Previous Tag is used for Release Note only - showing the file / commit differences between two tags. - Target Tag means the target deployment version, while Previous Tag means the latest version as of now. Previous Tag is used for Release Note only - showing the file / commit differences between two tags.
### Tagging
The naming convention would be following given the actual release tag is `0.100.0`
- `0.100.0-beta.1` (first version of pre-release)
- `0.100.0-beta.2` (include bug fix changes on top of the previous version)
- `0.100.0-beta.3`(include bug fix changes on top of the previous version)
- and so on ...
- `0.100.0` (actual release)
- `0.100.1` (minor bug fix release)
- `0.100.2` (minor bug fix release)
### Case 1: Leaving inputs blank ### Case 1: Leaving inputs blank
- If Previous Tag is blank, then the value will be fetched from [latest](https://github.com/nocodb/nocodb/releases/latest) - If Previous Tag is blank, then the value will be fetched from [latest](https://github.com/nocodb/nocodb/releases/latest)
- If Target Tag is blank, then the value will be Previous Tag plus one. Example: 0.90.11 (Previous Tag) + 1 = 0.90.12 (Target Tag) - If Target Tag is blank, then the value will be Previous Tag plus one. Example: 0.90.11 (Previous Tag) + 0.0.1 = 0.90.12 (Target Tag)
### Case 2: Manually Input ### Case 2: Manually Input

11
packages/nocodb/package-lock.json generated

@ -23,6 +23,7 @@
"bullmq": "^1.81.1", "bullmq": "^1.81.1",
"clear": "^0.1.0", "clear": "^0.1.0",
"colors": "1.4.0", "colors": "1.4.0",
"compare-versions": "^5.0.1",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^1.8.2", "cron": "^1.8.2",
@ -3810,6 +3811,11 @@
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true "dev": true
}, },
"node_modules/compare-versions": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.1.tgz",
"integrity": "sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ=="
},
"node_modules/component-emitter": { "node_modules/component-emitter": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -20757,6 +20763,11 @@
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true "dev": true
}, },
"compare-versions": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.1.tgz",
"integrity": "sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ=="
},
"component-emitter": { "component-emitter": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",

1
packages/nocodb/package.json

@ -63,6 +63,7 @@
"bullmq": "^1.81.1", "bullmq": "^1.81.1",
"clear": "^0.1.0", "clear": "^0.1.0",
"colors": "1.4.0", "colors": "1.4.0",
"compare-versions": "^5.0.1",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^1.8.2", "cron": "^1.8.2",

45
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts

@ -8,6 +8,7 @@ import { XKnex } from '../../../index';
import LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn';
import LookupColumn from '../../../../../models/LookupColumn'; import LookupColumn from '../../../../../models/LookupColumn';
import { jsepCurlyHook, UITypes } from 'nocodb-sdk'; import { jsepCurlyHook, UITypes } from 'nocodb-sdk';
import { validateDateWithUnknownFormat } from '../helpers/formulaFnHelper';
// todo: switch function based on database // todo: switch function based on database
@ -55,8 +56,11 @@ export default async function formulaQueryBuilderv2(
jsep.plugins.register(jsepCurlyHook); jsep.plugins.register(jsepCurlyHook);
const tree = jsep(_tree); const tree = jsep(_tree);
const columnIdToUidt = {};
// todo: improve - implement a common solution for filter, sort, formula, etc // todo: improve - implement a common solution for filter, sort, formula, etc
for (const col of await model.getColumns()) { for (const col of await model.getColumns()) {
columnIdToUidt[col.id] = col.uidt;
if (col.id in aliasToColumn) continue; if (col.id in aliasToColumn) continue;
switch (col.uidt) { switch (col.uidt) {
case UITypes.Formula: case UITypes.Formula:
@ -659,6 +663,47 @@ export default async function formulaQueryBuilderv2(
const right = fn(pt.right, null, pt.operator).toQuery(); const right = fn(pt.right, null, pt.operator).toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`; let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// comparing a date with empty string would throw
// `ERROR: zero-length delimited identifier` in Postgres
if (
knex.clientType() === 'pg' &&
columnIdToUidt[pt.left.name] === UITypes.Date
) {
// The correct way to compare with Date should be using
// `IS_AFTER`, `IS_BEFORE`, or `IS_SAME`
// This is to prevent empty data returned to UI due to incorrect SQL
if (pt.right.value === '') {
if (pt.operator === '=') {
sql = `${left} IS NULL ${colAlias}`;
} else {
sql = `${left} IS NOT NULL ${colAlias}`;
}
} else if (!validateDateWithUnknownFormat(pt.right.value)) {
// left tree value is date but right tree value is not date
// return true if left tree value is not null, else false
sql = `${left} IS NOT NULL ${colAlias}`;
}
}
if (
knex.clientType() === 'pg' &&
columnIdToUidt[pt.right.name] === UITypes.Date
) {
// The correct way to compare with Date should be using
// `IS_AFTER`, `IS_BEFORE`, or `IS_SAME`
// This is to prevent empty data returned to UI due to incorrect SQL
if (pt.left.value === '') {
if (pt.operator === '=') {
sql = `${right} IS NULL ${colAlias}`;
} else {
sql = `${right} IS NOT NULL ${colAlias}`;
}
} else if (!validateDateWithUnknownFormat(pt.left.value)) {
// right tree value is date but left tree value is not date
// return true if right tree value is not null, else false
sql = `${right} IS NOT NULL ${colAlias}`;
}
}
// handle NULL values when calling CONCAT for sqlite3 // handle NULL values when calling CONCAT for sqlite3
if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') { if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') {
sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`; sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`;

29
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts

@ -1,3 +1,7 @@
import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
extend(customParseFormat);
export function getWeekdayByText(v: string) { export function getWeekdayByText(v: string) {
return { return {
monday: 0, monday: 0,
@ -21,3 +25,28 @@ export function getWeekdayByIndex(idx: number): string {
6: 'sunday', 6: 'sunday',
}[idx || 0]; }[idx || 0];
} }
export function validateDateWithUnknownFormat(v: string) {
const dateFormats = [
'DD-MM-YYYY',
'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
];
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true;
}
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true;
}
}
}
return false;
}

2
packages/nocodb/src/lib/meta/api/tableApis.ts

@ -230,7 +230,7 @@ export async function tableUpdate(req: Request<any, any>, res) {
const project = await Project.getWithInfo(req.body.project_id); const project = await Project.getWithInfo(req.body.project_id);
const base = project.bases.find((b) => b.id === model.base_id); const base = project.bases.find((b) => b.id === model.base_id);
if (!req.body.table_name) { if (!req.body.table_name) {
NcError.badRequest( NcError.badRequest(
'Missing table name `table_name` property in request body' 'Missing table name `table_name` property in request body'

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

@ -1,5 +1,6 @@
// // Project CRUD // // Project CRUD
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { compareVersions, validate } from 'compare-versions';
import { ViewTypes } from 'nocodb-sdk'; import { ViewTypes } from 'nocodb-sdk';
import Project from '../../models/Project'; import Project from '../../models/Project';
@ -65,17 +66,22 @@ export async function versionInfo(_req: Request, res: Response) {
(versionCache.lastFetched && (versionCache.lastFetched &&
versionCache.lastFetched < Date.now() - 1000 * 60 * 60) versionCache.lastFetched < Date.now() - 1000 * 60 * 60)
) { ) {
versionCache.releaseVersion = await axios const nonBetaTags = await axios
.get('https://github.com/nocodb/nocodb/releases/latest', { .get('https://api.github.com/repos/nocodb/nocodb/tags', {
timeout: 5000, timeout: 5000,
}) })
.then((response) => .then((response) => {
response.request.res.responseUrl.replace( return response.data
'https://github.com/nocodb/nocodb/releases/tag/', .map((x) => x.name)
'' .filter(
) (v) => validate(v) && !v.includes('finn') && !v.includes('beta')
) )
.sort((x, y) => compareVersions(y, x));
})
.catch(() => null); .catch(() => null);
if (nonBetaTags && nonBetaTags.length > 0) {
versionCache.releaseVersion = nonBetaTags[0];
}
versionCache.lastFetched = Date.now(); versionCache.lastFetched = Date.now();
} }

Loading…
Cancel
Save