Browse Source

Merge branch 'develop' into refactor/pnpm

pull/9605/head
WK Wong 1 month ago
parent
commit
3d120e2d7a
  1. 2
      packages/nc-gui/components/cell/Currency.vue
  2. 2
      packages/nc-gui/components/cell/Decimal.vue
  3. 2
      packages/nc-gui/components/cell/Duration.vue
  4. 2
      packages/nc-gui/components/cell/Email.vue
  5. 2
      packages/nc-gui/components/cell/Float.vue
  6. 2
      packages/nc-gui/components/cell/Integer.vue
  7. 1
      packages/nc-gui/components/cell/Json.vue
  8. 2
      packages/nc-gui/components/cell/Percent.vue
  9. 2
      packages/nc-gui/components/cell/PhoneNumber.vue
  10. 1
      packages/nc-gui/components/cell/RichText.vue
  11. 2
      packages/nc-gui/components/cell/Text.vue
  12. 3
      packages/nc-gui/components/cell/TextArea.vue
  13. 2
      packages/nc-gui/components/cell/Url.vue
  14. 5
      packages/nc-gui/components/dashboard/TreeView/index.vue
  15. 2
      packages/nc-gui/components/general/FullScreen.vue
  16. 3
      packages/nc-gui/components/nc/ErrorBoundary.vue
  17. 14
      packages/nc-gui/components/smartsheet/details/Fields.vue
  18. 4
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  19. 38
      packages/nc-gui/components/smartsheet/form/field-config-error.vue
  20. 124
      packages/nc-gui/components/smartsheet/form/field-settings/visibility.vue
  21. 46
      packages/nc-gui/composables/useFormViewStore.ts
  22. 1
      packages/nc-gui/composables/useGlobal/types.ts
  23. 421
      packages/nc-gui/lib/form.ts
  24. 2
      packages/nc-gui/nuxt-shim.d.ts
  25. 92
      packages/nc-gui/plugins/error-reporting.ts
  26. 15
      packages/nc-gui/plugins/sentry.ts
  27. 4
      packages/nc-gui/store/projectsShortcuts.ts
  28. 9
      packages/nc-gui/utils/browserUtils.ts
  29. 6
      packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md
  30. 25
      packages/nocodb-sdk/src/lib/Api.ts
  31. 23
      packages/nocodb/README.md
  32. 6
      packages/nocodb/src/Noco.ts
  33. 20
      packages/nocodb/src/controllers/utils.controller.ts
  34. 14
      packages/nocodb/src/helpers/initBaseBehaviour.ts
  35. 7
      packages/nocodb/src/helpers/initDataSourceEncryption.ts
  36. 9
      packages/nocodb/src/models/Filter.ts
  37. 42
      packages/nocodb/src/schema/swagger.json
  38. 2
      packages/nocodb/src/services/bases.service.ts
  39. 20
      packages/nocodb/src/services/utils.service.ts
  40. 7
      packages/nocodb/src/utils/encryptDecrypt.ts
  41. 21
      packages/nocodb/src/utils/getInstance.ts
  42. 4
      packages/nocodb/src/version-upgrader/upgraders/0225002_ncDatasourceDecrypt.ts
  43. 2
      tests/playwright/tests/db/views/viewForm.spec.ts

2
packages/nc-gui/components/cell/Currency.vue

@ -101,6 +101,7 @@ onMounted(() => {
{{ currencyMeta.currency_code }}
</span>
</div>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn && editEnabled)"
:ref="focus"
@ -117,6 +118,7 @@ onMounted(() => {
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
@contextmenu.stop

2
packages/nc-gui/components/cell/Decimal.vue

@ -98,6 +98,7 @@ watch(isExpandedFormOpen, () => {
</script>
<template>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled"
:ref="focus"
@ -113,6 +114,7 @@ watch(isExpandedFormOpen, () => {
@keydown.right.stop
@keydown.up.stop="onKeyDown"
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

2
packages/nc-gui/components/cell/Duration.vue

@ -76,6 +76,7 @@ const focus: VNodeRef = (el) =>
<template>
<div class="duration-cell-wrapper">
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled"
:ref="focus"
@ -90,6 +91,7 @@ const focus: VNodeRef = (el) =>
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

2
packages/nc-gui/components/cell/Email.vue

@ -76,6 +76,7 @@ watch(
</script>
<template>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled"
:ref="focus"
@ -87,6 +88,7 @@ watch(
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
@paste.prevent="onPaste"

2
packages/nc-gui/components/cell/Float.vue

@ -46,6 +46,7 @@ const focus: VNodeRef = (el) =>
</script>
<template>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="editEnabled"
:ref="focus"
@ -59,6 +60,7 @@ const focus: VNodeRef = (el) =>
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

2
packages/nc-gui/components/cell/Integer.vue

@ -93,6 +93,7 @@ function onKeyDown(e: any) {
</script>
<template>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled"
:ref="focus"
@ -107,6 +108,7 @@ function onKeyDown(e: any) {
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

1
packages/nc-gui/components/cell/Json.vue

@ -214,6 +214,7 @@ watch(inputWrapperRef, () => {
:auto-focus="!isForm && !isEditColumn"
@update:model-value="localValue = $event"
@keydown.enter.stop
@keydown.alt.stop
/>
<span v-if="error" class="nc-cell-field text-xs w-full py-1 text-red-500">

2
packages/nc-gui/components/cell/Percent.vue

@ -139,6 +139,7 @@ const onTabPress = (e: KeyboardEvent) => {
@mouseleave="onMouseleave"
@focus="onWrapperFocus"
>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled && (isExpandedFormOpen ? expandedEditEnabled || !percentMeta.is_progress : true)"
:ref="focus"
@ -154,6 +155,7 @@ const onTabPress = (e: KeyboardEvent) => {
@keydown.up.stop
@keydown.delete.stop
@keydown.tab="onTabPress"
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

2
packages/nc-gui/components/cell/PhoneNumber.vue

@ -60,6 +60,7 @@ watch(
</script>
<template>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled"
:ref="focus"
@ -71,6 +72,7 @@ watch(
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

1
packages/nc-gui/components/cell/RichText.vue

@ -335,6 +335,7 @@ onClickOutside(editorDom, (e) => {
[`!overflow-hidden nc-truncate nc-line-clamp-${rowHeightTruncateLines(localRowHeight)}`]:
!fullMode && readOnly && localRowHeight && !isExpandedFormOpen && !isForm,
}"
@keydown.alt.stop
@keydown.alt.enter.stop
@keydown.shift.enter.stop
/>

2
packages/nc-gui/components/cell/Text.vue

@ -30,6 +30,7 @@ const focus: VNodeRef = (el) =>
</script>
<template>
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled"
:ref="focus"
@ -41,6 +42,7 @@ const focus: VNodeRef = (el) =>
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

3
packages/nc-gui/components/cell/TextArea.vue

@ -242,6 +242,7 @@ watch(inputWrapperRef, () => {
>
<LazyCellRichText v-model:value="vModel" sync-value-change read-only />
</div>
<!-- eslint-disable vue/use-v-on-exact -->
<textarea
v-else-if="(editEnabled && !isVisible) || isForm"
:ref="focus"
@ -259,6 +260,7 @@ watch(inputWrapperRef, () => {
}"
:disabled="readOnly"
@blur="editEnabled = false"
@keydown.alt.stop
@keydown.alt.enter.stop
@keydown.shift.enter.stop
@keydown.down.stop
@ -359,6 +361,7 @@ watch(inputWrapperRef, () => {
:style="{ resize: 'both' }"
:disabled="readOnly"
@keydown.escape="isVisible = false"
@keydown.alt.stop
/>
</div>

2
packages/nc-gui/components/cell/Url.vue

@ -81,6 +81,7 @@ watch(
<template>
<div class="flex flex-row items-center justify-between w-full h-full">
<!-- eslint-disable vue/use-v-on-exact -->
<input
v-if="!readOnly && editEnabled"
:ref="focus"
@ -92,6 +93,7 @@ watch(
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop
@mousedown.stop
/>

5
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -151,6 +151,11 @@ const isCreateTableAllowed = computed(
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (isActiveInputElementExist()) {
return
}
if (e.altKey && !e.shiftKey && !cmdOrCtrl) {
switch (e.keyCode) {
case 84: {

2
packages/nc-gui/components/general/FullScreen.vue

@ -19,7 +19,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
switch (e.keyCode) {
case 70: {
// ALT + F
if (!isDrawerOrModalExist()) {
if (!isDrawerOrModalExist() && !isActiveInputElementExist()) {
$e('c:shortcut', { key: 'ALT + F' })
isSidebarsOpen.value = !isSidebarsOpen.value
}

3
packages/nc-gui/components/nc/ErrorBoundary.vue

@ -1,7 +1,6 @@
<script lang="ts">
// modified version of default NuxtErrorBoundary component - https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-error-boundary.ts
import { message } from 'ant-design-vue'
import * as Sentry from '@sentry/vue'
const MESSAGE_KEY = 'ErrorMessageKey'
@ -54,7 +53,7 @@ export default {
}, 30000)
try {
Sentry.captureException(err)
nuxtApp.$report(err)
} catch {
// ignore
}

14
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -895,12 +895,7 @@ onKeyDown('ArrowUp', () => {
onKeyDown('Delete', () => {
if (isLocked.value) return
if (
document.activeElement?.tagName === 'INPUT' ||
document.activeElement?.tagName === 'TEXTAREA' ||
// A rich text editor is a div with the contenteditable attribute set to true.
document.activeElement?.getAttribute('contenteditable')
) {
if (isActiveInputElementExist()) {
return
}
@ -913,12 +908,7 @@ onKeyDown('Delete', () => {
onKeyDown('Backspace', () => {
if (isLocked.value) return
if (
document.activeElement?.tagName === 'INPUT' ||
document.activeElement?.tagName === 'TEXTAREA' ||
// A rich text editor is a div with the contenteditable attribute set to true.
document.activeElement?.getAttribute('contenteditable')
) {
if (isActiveInputElementExist()) {
return
}

4
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -404,9 +404,13 @@ useActiveKeyupListener(
if (!e.altKey) return
if (e.key === 'ArrowLeft') {
e.stopPropagation()
if (isFirstRow.value) return
loadingEmit('prev')
} else if (e.key === 'ArrowRight') {
e.stopPropagation()
if (islastRow.value) return
onNext()
}
// on alt + s save record

38
packages/nc-gui/components/smartsheet/form/field-config-error.vue

@ -5,7 +5,41 @@ interface Props {
column: ColumnType
mode: 'preview' | 'list'
}
defineProps<Props>()
const props = defineProps<Props>()
const { column, mode } = toRefs(props)
const visibilityError = computed(() => {
return parseProp(column.value?.meta)?.visibility?.errors || {}
})
const firstErrorMsg = computed(() => {
const visibilityErr = Object.values(visibilityError.value ?? [])
if (visibilityErr.length) {
return visibilityErr[0]
}
})
</script>
<template><div></div></template>
<template>
<template v-if="mode === 'preview'">
<div v-if="Object.keys(visibilityError ?? {}).length" class="flex mt-2">
<NcTooltip :disabled="!firstErrorMsg" class="flex cursor-pointer" placement="bottom">
<template #title>
<div class="flex flex-col">
{{ firstErrorMsg }}
</div>
</template>
<div
class="nc-field-config-error validation-error text-[#CB3F36] bg-[#FFF2F1] rounded-lg inline-flex items-center gap-2 px-2 py-1"
>
<GeneralIcon icon="alertTriangle" />
<div class="flex">Configuration error</div>
</div>
</NcTooltip>
</div>
</template>
<template v-else>
<GeneralIcon v-if="Object.keys(visibilityError ?? {}).length" icon="alertTriangle" class="ml-1 flex-none !text-red-500" />
</template>
</template>

124
packages/nc-gui/components/smartsheet/form/field-settings/visibility.vue

@ -1,7 +1,46 @@
<script lang="ts" setup>
const { activeField } = useFormViewStoreOrThrow()
import type { ColumnType } from 'nocodb-sdk'
const { visibleColumns, activeField, allViewFilters, localColumnsMapByFkColumnId } = useFormViewStoreOrThrow()
const isOpen = ref<boolean>(false)
const allFilters = ref({})
provide(AllFiltersInj, allFilters)
const visibilityError = computed(() => {
return parseProp(activeField.value?.meta)?.visibility?.errors || {}
})
const hasvisibilityError = computed(() => Object.keys(visibilityError.value).length)
const visibilityFilters = computed(() => {
if (activeField.value?.fk_column_id && !allViewFilters.value[activeField.value.fk_column_id]) {
allViewFilters.value[activeField.value.fk_column_id] = []
}
return allViewFilters.value[`${activeField.value?.fk_column_id}`]
})
const isFirstField = computed(() => {
return !!(visibleColumns.value.length && visibleColumns.value[0].id === activeField.value?.id)
})
const filterOption = (column: ColumnType) => {
// hide active field from filter option
const isNotActiveField = column.id !== activeField.value?.fk_column_id
// show only form view visible columns and order is less than active field
const orderIsLessThanActiveField =
column.id && localColumnsMapByFkColumnId.value[column.id]
? (localColumnsMapByFkColumnId.value[column.id].order ?? Infinity) < (activeField.value?.order ?? Infinity)
: false
const isVisible = localColumnsMapByFkColumnId.value[column.id]?.show
return isNotActiveField && orderIsLessThanActiveField && isVisible
}
</script>
<template>
@ -11,28 +50,79 @@ const isOpen = ref<boolean>(false)
<div class="text-gray-800 font-medium">{{ $t('labels.showOnConditions') }}</div>
<div class="flex flex-col">
<NcTooltip placement="left">
<template #title>
<div class="text-center">
{{ $t('msg.info.thisFeatureIsOnlyAvailableInEnterpriseEdition') }}
<NcDropdown
v-if="visibilityFilters"
v-model:visible="isOpen"
placement="bottomLeft"
:disabled="isFirstField && !visibilityFilters.length && !isOpen"
overlay-class-name="nc-form-field-visibility-dropdown"
>
<NcTooltip placement="left" :disabled="!isFirstField">
<template #title> Cannot add conditions to the first field in a form. </template>
<div
class="nc-form-field-visibility-btn border-1 rounded-lg py-1 px-3 flex items-center justify-between gap-2 !min-w-[170px] transition-all cursor-pointer select-none text-sm"
:class="{
'!border-brand-500 shadow-selected': isOpen,
'border-gray-200': !isOpen,
'bg-[#F0F3FF]': visibilityFilters.length,
'opacity-70 cursor-default nc-disabled': isFirstField && !visibilityFilters.length,
}"
data-testid="nc-form-field-visibility-btn"
>
<div
class="nc-form-field-visibility-conditions-count flex-1"
:class="{
'text-brand-500 ': visibilityFilters.length,
}"
>
{{
visibilityFilters.length
? `${visibilityFilters.length} condition${visibilityFilters.length !== 1 ? 's' : ''}`
: 'No conditions'
}}
</div>
<GeneralIcon v-if="hasvisibilityError" icon="alertTriangle" class="flex-none !text-red-500" />
<GeneralIcon
icon="settings"
class="flex-none w-4 h-4"
:class="{
'text-brand-500 ': visibilityFilters.length,
}"
/>
</div>
</NcTooltip>
<template #overlay>
<div
class="nc-form-field-visibility-dropdown-container"
:class="{
'py-2': !visibilityFilters.length,
}"
>
<SmartsheetToolbarColumnFilter
ref="fieldVisibilityRef"
v-model="visibilityFilters"
class="w-full"
:auto-save="true"
data-testid="nc-filter-menu"
:show-loading="false"
:parent-col-id="activeField.fk_column_id"
:filter-option="filterOption"
:visibility-error="visibilityError"
:disable-add-new-filter="isFirstField"
/>
</div>
</template>
<div
class="nc-form-field-visibility-btn border-1 rounded-lg py-1 px-3 flex items-center justify-between gap-2 !min-w-[170px] transition-all select-none text-sm opacity-50 cursor-not-allowed"
:class="{
'!border-brand-500': isOpen,
'border-gray-200': !isOpen,
}"
>
<div class="nc-form-field-visibility-conditions-count flex-1">No conditions</div>
<GeneralIcon icon="settings" class="flex-none w-4 h-4" />
</div>
</NcTooltip>
</NcDropdown>
</div>
</div>
<div>
<div class="text-sm text-gray-500">{{ $t('labels.showFieldOnConditionsMet') }}</div>
<div v-if="hasvisibilityError" class="mt-2 visibility-condition-input-error text-red-500">
Error conditions will not be used for determining field visibility.
</div>
</div>
</div>
</div>

46
packages/nc-gui/composables/useFormViewStore.ts

@ -18,6 +18,10 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
const { t } = useI18n()
const { isMysql } = useBase()
const { getMeta } = useMetas()
const formResetHook = createEventHook<void>()
const allViewFilters = ref<Record<string, FilterType[]>>({})
@ -36,6 +40,17 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
}, {} as Record<string, ColumnType & Record<string, any>>)
})
const fieldVisibilityValidator = computed(() => {
return new FormFilters({
nestedGroupedFilters: allViewFilters.value,
formViewColumns: localColumns.value,
formViewColumnsMapByFkColumnId: localColumnsMapByFkColumnId.value,
formState: formState.value,
isMysql,
getMeta,
})
})
const visibleColumns = computed(() =>
localColumns.value.filter((f) => f.show).sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)),
)
@ -71,7 +86,7 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
{
validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => {
if (isRequired(column, column.required)) {
if (isRequired(column, column.required) && column.visible) {
if (typeof value === 'string') {
value = value.trim()
}
@ -88,6 +103,14 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
})
},
},
{
validator: (_rule: RuleObject) => {
return new Promise((resolve) => {
checkFieldVisibility()
return resolve()
})
},
},
]
const additionalRules = extractFieldValidator(parseProp(column.meta).validators ?? [], column)
@ -179,9 +202,26 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
return required || (columnObj && columnObj.rqd && !columnObj.cdf)
}
const loadAllviewFilters = async () => {}
const loadAllviewFilters = async () => {
if (!viewMeta.value?.id) return
try {
const formViewFilters = (await $api.dbTableFilter.read(viewMeta.value.id, { includeAllFilters: true })).list || []
if (!formViewFilters.length) return
const formFilter = new FormFilters({ data: formViewFilters })
const allFilters = formFilter.getNestedGroupedFilters()
function checkFieldVisibility() {}
allViewFilters.value = { ...allFilters }
} catch (e: any) {
console.error('Error loading view filters:', e)
}
}
async function checkFieldVisibility() {
await fieldVisibilityValidator.value.validateVisibility()
}
return {
onReset: formResetHook.on,

1
packages/nc-gui/composables/useGlobal/types.ts

@ -40,6 +40,7 @@ export interface AppInfo {
samlProviderName: string | null
giftUrl: string
feedEnabled: boolean
sentryDSN: string
}
export interface StoredState {

421
packages/nc-gui/lib/form.ts

@ -1,5 +1,6 @@
import type { ColumnType } from 'ant-design-vue/lib/table'
import { type FilterType } from 'nocodb-sdk'
import dayjs from 'dayjs'
import { type FilterType, type LinkToAnotherRecordType, type TableType, UITypes, isDateMonthFormat } from 'nocodb-sdk'
type FormViewColumn = ColumnType & Record<string, any>
@ -11,7 +12,9 @@ export class FormFilters {
formViewColumnsMapByFkColumnId: Record<string, FormViewColumn>
formState: Record<string, any>
value: any
isSharedForm: boolean
isMysql?: (sourceId?: string) => boolean
getMeta?: (tableIdOrTitle: string) => Promise<TableType | null>
constructor({
data = [],
@ -20,6 +23,8 @@ export class FormFilters {
formViewColumnsMapByFkColumnId = {},
formState = {},
isMysql = undefined,
isSharedForm = false,
getMeta = undefined,
}: {
data?: FilterType[]
nestedGroupedFilters?: Record<string, FilterType[]>
@ -27,6 +32,8 @@ export class FormFilters {
formViewColumnsMapByFkColumnId?: Record<string, FormViewColumn>
formState?: Record<string, any>
isMysql?: (sourceId?: string) => boolean
isSharedForm?: boolean
getMeta?: (tableIdOrTitle: string) => Promise<TableType | null>
} = {}) {
this.allViewFilters = data
this.groupedFilters = {}
@ -34,14 +41,422 @@ export class FormFilters {
this.formViewColumns = formViewColumns
this.formViewColumnsMapByFkColumnId = formViewColumnsMapByFkColumnId
this.formState = formState
this.isSharedForm = isSharedForm
this.isMysql = isMysql
this.getMeta = getMeta
}
setFilters(filters: FilterType[]) {
this.allViewFilters = filters
}
getNestedGroupedFilters() {}
getRootFilters(parentColId: string) {
return (this.groupedFilters[parentColId] || [])
.filter((f) => !f.fk_parent_id)
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
}
getParentFilters(parentColId: string, parentId: string) {
return (this.groupedFilters[parentColId] || [])
.filter((f) => f.fk_parent_id === parentId)
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
}
getAllChildFilters(filters: FilterType[], parentColId: string): any {
return filters.map((filter) => {
if (filter.id && filter.is_group) {
const childFilters = this.getParentFilters(parentColId, filter.id)
filter.children = this.getAllChildFilters(childFilters, parentColId)
}
return filter
})
}
loadFilters() {
for (const parentColId in this.groupedFilters) {
const rootFilters = this.getRootFilters(parentColId)
this.nestedGroupedFilters[parentColId] = this.getAllChildFilters(rootFilters, parentColId)
}
return this.nestedGroupedFilters
}
// Method to group filters by fk_parent_column_id
getNestedGroupedFilters() {
const groupedFilters = this.allViewFilters.reduce((acc, filter) => {
const groupingKey = filter.fk_parent_column_id || 'ungrouped'
if (!acc[groupingKey]) {
acc[groupingKey] = []
}
acc[groupingKey].push(filter)
return acc
}, {} as typeof this.groupedFilters)
this.groupedFilters = groupedFilters
const nestedGroupedFilters = this.loadFilters()
return nestedGroupedFilters
}
toString(value: any) {
return `${value || ''}`
}
isFieldAboveParentColumn(column: FormViewColumn, parentColumn: FormViewColumn) {
return column.order < parentColumn.order
}
async getOoOrBtColVal(column: FormViewColumn) {
const fk_related_model_id = (column?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id
if (!fk_related_model_id || typeof this.getMeta !== 'function') return null
const relatedTableMeta = await this.getMeta(fk_related_model_id)
if (!relatedTableMeta || !Array.isArray(relatedTableMeta?.columns)) return null
const displayValTitle = (relatedTableMeta.columns.find((c) => c.pv) || relatedTableMeta.columns?.[0])?.title || ''
if (
!displayValTitle ||
!this.formState[column.title] ||
!ncIsObject(this.formState[column.title]) ||
this.formState[column.title][displayValTitle] === undefined
) {
return null
}
return this.formState[column.title][displayValTitle]
}
async validateCondition(
filters: FilterType[] = [],
parentCol: FormViewColumn,
errors: Record<string, string>,
): Promise<boolean | undefined> {
if (!filters.length) {
return true
}
let isValid
for (const filter of filters) {
let res
if (filter.is_group) {
res = await this.validateCondition(filter.children, parentCol, errors)
} else {
if (!filter.fk_column_id || !this.formViewColumnsMapByFkColumnId[filter.fk_column_id]) {
res = false
}
const column = this.formViewColumnsMapByFkColumnId[filter.fk_column_id]
// If the filter condition col is below parent column then this will be invalid condition so return false
if (!this.isFieldAboveParentColumn(column, parentCol)) {
errors[column.fk_column_id] = `Condition references a field (${column.title}) that comes later in the form.`
res = true
}
if (!column.show) {
errors[column.fk_column_id] = `Condition references a field (${column.title}) that was removed from the form.`
res = true
}
if (!column?.visible) {
res = false
}
const field = column.title
let val = this.formState[field]
if (res === undefined) {
if (
[UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(column.uidt) &&
!['empty', 'blank', 'notempty', 'notblank'].includes(filter.comparison_op)
) {
const dateFormat = this.isMysql?.(column.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
let now = dayjs(new Date())
const dateFormatFromMeta = column?.meta?.date_format
const dataVal: any = val
let filterVal: any = filter.value
if (dateFormatFromMeta && isDateMonthFormat(dateFormatFromMeta)) {
// reset to 1st
now = dayjs(now).date(1)
if (val) val = dayjs(val).date(1)
}
if (filterVal) res = dayjs(filterVal).isSame(dataVal, 'day')
// handle sub operation
switch (filter.comparison_sub_op) {
case 'today':
filterVal = now
break
case 'tomorrow':
filterVal = now.add(1, 'day')
break
case 'yesterday':
filterVal = now.add(-1, 'day')
break
case 'oneWeekAgo':
filterVal = now.add(-1, 'week')
break
case 'oneWeekFromNow':
filterVal = now.add(1, 'week')
break
case 'oneMonthAgo':
filterVal = now.add(-1, 'month')
break
case 'oneMonthFromNow':
filterVal = now.add(1, 'month')
break
case 'daysAgo':
if (!filterVal) return
filterVal = now.add(-filterVal, 'day')
break
case 'daysFromNow':
if (!filterVal) return
filterVal = now.add(filterVal, 'day')
break
case 'exactDate':
if (!filterVal) return
break
// sub-ops for `isWithin` comparison
case 'pastWeek':
filterVal = now.add(-1, 'week')
break
case 'pastMonth':
filterVal = now.add(-1, 'month')
break
case 'pastYear':
filterVal = now.add(-1, 'year')
break
case 'nextWeek':
filterVal = now.add(1, 'week')
break
case 'nextMonth':
filterVal = now.add(1, 'month')
break
case 'nextYear':
filterVal = now.add(1, 'year')
break
case 'pastNumberOfDays':
if (!filterVal) return
filterVal = now.add(-filterVal, 'day')
break
case 'nextNumberOfDays':
if (!filterVal) return
filterVal = now.add(filterVal, 'day')
break
}
if (dataVal) {
switch (filter.comparison_op) {
case 'eq':
res = dayjs(dataVal).isSame(filterVal, 'day')
break
case 'neq':
res = !dayjs(dataVal).isSame(filterVal, 'day')
break
case 'gt':
res = dayjs(dataVal).isAfter(filterVal, 'day')
break
case 'lt':
res = dayjs(dataVal).isBefore(filterVal, 'day')
break
case 'lte':
case 'le':
res = dayjs(dataVal).isSameOrBefore(filterVal, 'day')
break
case 'gte':
case 'ge':
res = dayjs(dataVal).isSameOrAfter(filterVal, 'day')
break
case 'empty':
case 'blank':
res = dataVal === '' || dataVal === null || dataVal === undefined
break
case 'notempty':
case 'notblank':
res = !(dataVal === '' || dataVal === null || dataVal === undefined)
break
case 'isWithin': {
let now = dayjs(new Date()).format(dateFormat).toString()
now = column.uidt === UITypes.Date ? now.substring(0, 10) : now
switch (filter.comparison_sub_op) {
case 'pastWeek':
case 'pastMonth':
case 'pastYear':
case 'pastNumberOfDays':
res = dayjs(dataVal).isBetween(filterVal, now, 'day')
break
case 'nextWeek':
case 'nextMonth':
case 'nextYear':
case 'nextNumberOfDays':
res = dayjs(dataVal).isBetween(now, filterVal, 'day')
break
}
}
}
}
} else {
switch (typeof filter.value) {
case 'boolean':
val = !!this.formState[field]
break
case 'number':
val = Number.isNaN(parseFloat(this.formState[field])) ? this.formState[field] : +this.formState[field]
break
}
switch (column.uidt) {
case UITypes.Links:
if (isMm(column) || isHm(column)) {
val = (this.formState[field] ?? []).length
}
break
case UITypes.LinkToAnotherRecord:
if (isOo(column) || isBt(column)) {
val = await this.getOoOrBtColVal(column)
}
break
}
switch (filter.comparison_op) {
case 'eq':
// eslint-disable-next-line eqeqeq
res = val == filter.value
break
case 'neq':
// eslint-disable-next-line eqeqeq
res = val != filter.value
break
case 'like':
res = this.toString(val).toLowerCase()?.includes(filter.value?.toLowerCase())
break
case 'nlike':
res = !this.toString(val).toLowerCase()?.includes(filter.value?.toLowerCase())
break
case 'empty':
case 'blank':
res = val === '' || val === null || val === undefined
break
case 'notempty':
case 'notblank':
res = !(val === '' || val === null || val === undefined)
break
case 'checked':
res = !!val
break
case 'notchecked':
res = !val
break
case 'null':
res = val === null
break
case 'notnull':
res = val !== null
break
case 'allof':
res = (
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).every((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'anyof':
res = (
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).some((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'nallof':
res = !(
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).every((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'nanyof':
res = !(
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).some((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'lt':
res = parseFloat(val) < +filter.value
break
case 'lte':
case 'le':
res = parseFloat(val) <= +filter.value
break
case 'gt':
res = parseFloat(val) > +filter.value
break
case 'gte':
case 'ge':
res = parseFloat(val) >= +filter.value
break
}
}
}
}
switch (filter.logical_op) {
case 'or':
isValid = isValid || !!res
break
case 'not':
isValid = isValid && !res
break
case 'and':
default:
isValid = (isValid ?? true) && res
break
}
}
return isValid
}
async validateVisibility() {
const res: Record<string, boolean> = {}
for (const column of this.formViewColumns) {
const columnFilters = this.nestedGroupedFilters[column.fk_column_id] ?? []
const errors: Record<string, string> = {}
const isValid = await this.validateCondition(columnFilters, column, errors)
if (this.isSharedForm) {
if (!column.meta?.preFilledHiddenField) {
column.show = !!isValid
column.visible = !!isValid
}
} else {
column.visible = !!isValid
column.meta = {
...parseProp(column.meta),
visibility: {
errors,
},
}
}
}
return res
}
validateVisibility() {}
validateErrors() {}
}

2
packages/nc-gui/nuxt-shim.d.ts vendored

@ -12,6 +12,8 @@ declare module '#app' {
}
/** {@link import('./plugins/tele') Telemetry} Emit telemetry event */
$e: (event: string, data?: any) => void
/** {@link import('./plugins/report') Error reporting} Error reporting */
$report: (event: Error) => void
$state: UseGlobalReturn
$poller: {
subscribe(

92
packages/nc-gui/plugins/error-reporting.ts

@ -0,0 +1,92 @@
import * as Sentry from '@sentry/vue'
import type { Api } from 'nocodb-sdk'
class ErrorReporting {
errors: Error[] = []
// debounce error reporting to avoid sending multiple reports for the same error
private report = useDebounceFn(
() => {
try {
const errors = this.errors
// filter out duplicate errors and only include 2 lines of stack trace
.filter((error, index, self) => index === self.findIndex((t) => t.message === error.message))
.map((error) => ({
message: error.message,
stack: error.stack?.split('\n').slice(0, 2).join('\n'),
}))
this.errors = []
this.$api.utils.errorReport({ errors, extra: {} })
} catch {
// ignore
}
},
3000,
{
maxWait: 10000,
},
)
constructor(private $api: Api<unknown>) {}
// collect error to report later
collect(error: Error) {
this.errors.push(error)
// report errors after 3 seconds
this.report()
}
}
export default defineNuxtPlugin((nuxtApp) => {
if (isEeUI) {
nuxtApp.provide('report', function (error: Error) {
try {
Sentry.captureException(error)
} catch {
// ignore
}
})
return
}
const env = process.env.NODE_ENV === 'production' ? 'production' : 'development'
let isSentryConfigured = false
let isErrorReportingEnabled = false
let errorReporting: ErrorReporting | null = null
// load error reporting only if enabled and sentryDSN is not provided
watch(
[
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.errorReportingEnabled,
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.sentryDSN,
],
([enabled, sentryDSN]) => {
isSentryConfigured = enabled && !!sentryDSN
isErrorReportingEnabled = enabled
if (enabled && !sentryDSN) {
errorReporting = new ErrorReporting(nuxtApp.$api as Api<unknown>)
} else {
errorReporting = null
}
},
{ immediate: true },
)
function report(error: Error) {
if (process.env.CI || process.env.PLAYWRIGHT) {
return
}
if (env !== 'production' && !process.env.NC_ENABLE_DEV_SENTRY) {
return
}
if (isSentryConfigured) {
Sentry.captureException(error)
} else if (isErrorReportingEnabled) {
errorReporting?.collect(error)
}
}
nuxtApp.provide('report', report)
})

15
packages/nc-gui/plugins/sentry.ts

@ -20,14 +20,14 @@ export default defineNuxtPlugin((nuxtApp) => {
let initialized = false
const init = () => {
const init = (dsn: string) => {
// prevent multiple init
if (initialized) return
initialized = true
Sentry.init({
app: [vueApp],
dsn: 'https://64cb4904bcbec03a1b9a0be02a2d10a9@o4505953073889280.ingest.us.sentry.io/4507725383663616',
dsn,
environment: env,
integrations: [
new Sentry.BrowserTracing({
@ -56,11 +56,14 @@ export default defineNuxtPlugin((nuxtApp) => {
// load sentry only if enabled
watch(
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.errorReportingEnabled,
(enabled) => {
[
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.errorReportingEnabled,
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.sentryDSN,
],
([enabled, sentryDSN]) => {
try {
if (enabled) init()
} catch (e) {
if (enabled && sentryDSN) init(sentryDSN)
} catch {
// ignore
}
},

4
packages/nc-gui/store/projectsShortcuts.ts

@ -24,7 +24,7 @@ export const useProjectsShortcuts = defineStore('projectsShortcutsStore', () =>
switch (e.keyCode) {
case 70: {
// ALT + F
if (!isDrawerOrModalExist()) {
if (!isDrawerOrModalExist() && !isActiveInputElementExist()) {
$e('c:shortcut', { key: 'ALT + F' })
const sidebarStore = useSidebarStore()
@ -37,7 +37,7 @@ export const useProjectsShortcuts = defineStore('projectsShortcutsStore', () =>
}
// 'ALT + ,'
case 188: {
if (isUIAllowed('settingsPage') && !isDrawerOrModalExist()) {
if (isUIAllowed('settingsPage') && !isDrawerOrModalExist() && !isActiveInputElementExist()) {
$e('c:shortcut', { key: 'ALT + ,' })
const basesStore = useBases()

9
packages/nc-gui/utils/browserUtils.ts

@ -5,7 +5,14 @@ export const isDrawerOrModalExist = () => document.querySelector('.ant-modal.act
export const isExpandedFormOpenExist = () => document.querySelector('.nc-drawer-expanded-form.active')
export const isExpandedCellInputExist = () => document.querySelector('.expanded-cell-input')
export const cmdKActive = () => document.querySelector('.cmdk-modal-active')
export const isActiveInputElementExist = () => {
return (
document.activeElement?.tagName === 'INPUT' ||
document.activeElement?.tagName === 'TEXTAREA' ||
// A rich text editor is a div with the contenteditable attribute set to true.
!!document.activeElement?.getAttribute('contenteditable')
)
}
export const getScrollbarWidth = () => {
const outer = document.createElement('div')
outer.style.visibility = 'hidden'

6
packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md

@ -90,14 +90,14 @@ For production use cases, it is crucial to set all environment variables marked
| `NC_ALLOW_LOCAL_HOOKS` | No | Allows webhooks to call local network links, posing potential security risks. Set to `true` to enable; all other values are considered `false`. | Defaults to `false`. |
| `NC_SANITIZE_COLUMN_NAME` | No | Enables sanitization of column names during their creation to prevent SQL injection and other security issues. | Defaults to `true`. |
| `NC_TOOL_DIR` | No | Specifies the directory to store metadata and app-related files. In Docker setups, this maps to `/usr/app/data/` for mounting volumes. | Defaults to the current working directory. |
| `NC_DISABLE_BASE_AS_PG_SCHEMA` | No | Disables the creation of a schema for each base in PostgreSQL. [Click here for more detail](#postgres-base-as-schema) | |
| `NC_DISABLE_PG_DATA_REFLECTION` | No | Disables the creation of a schema for each base in PostgreSQL. [Click here for more detail](#postgres-data-reflection) | |
| `NC_MIGRATIONS_DISABLED` | No | Disables NocoDB migrations. | |
| `NC_DISABLE_AUDIT` | No | Disables the audit log feature. | Defaults to `false`. |
| `NC_AUTOMATION_LOG_LEVEL` | No | Configures logging levels for automation features. Possible values: `OFF`, `ERROR`, `ALL`. More details can be found under [Webhooks](/automation/webhook/create-webhook). | Defaults to `OFF`. |
### Postgres Base as Schema
### Postgres Data Reflection
For PostgreSQL, a unique schema is created for each base, providing logical separation within the database. This feature is enabled by default if the user has the required permissions. To disable it, set the `NC_DISABLE_BASE_AS_PG_SCHEMA` environment variable to `false`.
NocoDB UI is exactly what's in your Postgres database schema. Same tables, same columns—everything is perfectly mirrored. This is done by creating a schema for each base in PostgreSQL. This feature is enabled by default if the user has the required permissions. To disable it, set the `NC_DISABLE_PG_DATA_REFLECTION` environment variable to `false`.
## Logging & Monitoring

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

@ -3100,6 +3100,14 @@ export interface CalendarColumnReqType {
order?: number;
}
export interface ErrorReportReqType {
errors?: {
message?: string;
stack?: string;
}[];
extra?: object;
}
/**
* Model for Comment
*/
@ -10735,6 +10743,23 @@ export class Api<
}),
/**
* @description Error Reporting
*
* @tags Utils, Internal
* @name ErrorReport
* @summary Error Reporting
* @request POST:/api/v1/error-reporting
*/
errorReport: (data: any, params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/error-reporting`,
method: 'POST',
body: data,
type: ContentType.Json,
...params,
}),
/**
* @description Generic Axios Call
*
* @tags Utils

23
packages/nocodb/README.md

@ -63,7 +63,6 @@ NocoDB is the fastest and easiest way to build databases online.
# Quick try
## Docker
```bash
@ -81,7 +80,7 @@ docker run -d --name nocodb-postgres \
-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
> To persist data in docker you can mount volume at `/usr/app/data/` since 0.10.6. Otherwise your data will be lost after recreating the container.
@ -164,22 +163,22 @@ Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/
# Table of Contents
- [Quick try](#quick-try)
- [NPX](#npx)
- [Node Application](#node-application)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [NPX](#npx)
- [Node Application](#node-application)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [GUI](#gui)
- [Join Our Community](#join-our-community)
- [Screenshots](#screenshots)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
- [App Store for Workflow Automations](#app-store-for-workflow-automations)
- [Programmatic Access](#programmatic-access)
- [Sync Schema](#sync-schema)
- [Audit](#audit)
- [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
- [App Store for Workflow Automations](#app-store-for-workflow-automations)
- [Programmatic Access](#programmatic-access)
- [Sync Schema](#sync-schema)
- [Audit](#audit)
- [Production Setup](#production-setup)
- [Environment variables](#environment-variables)
- [Environment variables](#environment-variables)
- [Development Setup](#development-setup)
- [Contributing](#contributing)
- [Why are we building this?](#why-are-we-building-this)

6
packages/nocodb/src/Noco.ts

@ -12,10 +12,10 @@ import type { MetaService } from '~/meta/meta.service';
import type { IEventEmitter } from '~/modules/event-emitter/event-emitter.interface';
import type { Express } from 'express';
import type http from 'http';
import type Sharp from 'sharp';
import { MetaTable, RootScopes } from '~/utils/globals';
import { AppModule } from '~/app.module';
import { isEE, T } from '~/utils';
import type Sharp from 'sharp';
dotenv.config();
@ -106,7 +106,9 @@ export default class Noco {
try {
this.sharp = (await import('sharp')).default;
} catch {
console.error('Sharp is not available for your platform, thumbnail generation will be skipped');
console.error(
'Sharp is not available for your platform, thumbnail generation will be skipped',
);
}
if (process.env.NC_WORKER_CONTAINER === 'true') {

20
packages/nocodb/src/controllers/utils.controller.ts

@ -12,6 +12,7 @@ import {
} from '@nestjs/common';
import { ProjectRoles, validateAndExtractSSLProp } from 'nocodb-sdk';
import {
ErrorReportReqType,
getTestDatabaseName,
IntegrationsType,
OrgUserRoles,
@ -26,7 +27,7 @@ import { NcRequest } from '~/interface/config';
import { Integration } from '~/models';
import { MetaTable, RootScopes } from '~/utils/globals';
import { NcError } from '~/helpers/catchError';
import { deepMerge } from '~/utils';
import { deepMerge, isEE } from '~/utils';
import Noco from '~/Noco';
@Controller()
@ -173,4 +174,21 @@ export class UtilsController {
async feed(@Request() req: NcRequest) {
return await this.utilsService.feed(req);
}
@UseGuards(PublicApiLimiterGuard)
@Post('/api/v1/error-reporting')
async reportErrors(@Req() req: NcRequest, @Body() body: ErrorReportReqType) {
if (
`${process.env.NC_DISABLE_ERR_REPORTS}` === 'true' ||
isEE ||
process.env.NC_SENTRY_DSN
) {
return {};
}
return (await this.utilsService.reportErrors({
req,
body,
})) as any;
}
}

14
packages/nocodb/src/helpers/initBaseBehaviour.ts

@ -48,8 +48,8 @@ export async function initBaseBehavior() {
return;
}
// disable minimal databases feature if NC_DISABLE_BASE_AS_PG_SCHEMA is set to true
if (process.env.NC_DISABLE_BASE_AS_PG_SCHEMA === 'true') {
// disable minimal databases feature if NC_DISABLE_PG_DATA_REFLECTION is set to true
if (process.env.NC_DISABLE_PG_DATA_REFLECTION === 'true') {
return;
}
@ -64,8 +64,8 @@ export async function initBaseBehavior() {
// if schema creation is not allowed, return
if (!schemaCreateAllowed?.rows?.[0]?.has_database_privilege) {
// set NC_MINIMAL_DBS to false if it's set to true and log warning
process.env.NC_DISABLE_BASE_AS_PG_SCHEMA = 'true';
// set NC_DISABLE_PG_DATA_REFLECTION to true and log warning
process.env.NC_DISABLE_PG_DATA_REFLECTION = 'true';
logger.warn(
`User ${
(dataConfig.connection as PgConnectionConfig)?.user
@ -74,13 +74,13 @@ export async function initBaseBehavior() {
return;
}
// set NC_MINIMAL_DBS to true
process.env.NC_DISABLE_BASE_AS_PG_SCHEMA = 'false';
// set NC_DISABLE_PG_DATA_REFLECTION to false
process.env.NC_DISABLE_PG_DATA_REFLECTION = 'false';
} catch (error) {
logger.warn(
`Error while checking schema creation permission: ${error.message}`,
);
process.env.NC_DISABLE_BASE_AS_PG_SCHEMA = 'true';
process.env.NC_DISABLE_PG_DATA_REFLECTION = 'true';
} finally {
// close the connection since it's only used to verify permission
await tempConnection?.destroy();

7
packages/nocodb/src/helpers/initDataSourceEncryption.ts

@ -56,6 +56,8 @@ export default async function initDataSourceEncryption(_ncMeta = Noco.ncMeta) {
source.id,
);
successStatus.push(true);
logger.log(`Encrypted source ${source.alias}`);
}
const integrations = await ncMeta
@ -67,13 +69,13 @@ export default async function initDataSourceEncryption(_ncMeta = Noco.ncMeta) {
for (const integration of integrations) {
// skip if no config
if (!integrations.config) {
if (!integration.config) {
continue;
}
// check if valid json, if not warn and skip
try {
JSON.parse(integrations.config);
JSON.parse(integration.config);
} catch (e) {
logger.warn('Invalid JSON in integration config', integration.title);
successStatus.push(false);
@ -95,6 +97,7 @@ export default async function initDataSourceEncryption(_ncMeta = Noco.ncMeta) {
integration.id,
);
successStatus.push(true);
logger.log(`Encrypted integration config ${integration.title}`);
}
// if all failed, throw error

9
packages/nocodb/src/models/Filter.ts

@ -171,9 +171,14 @@ export default class Filter implements FilterType {
filter: Partial<FilterType>,
ncMeta = Noco.ncMeta,
) {
if (!(id && (filter.fk_view_id || filter.fk_hook_id))) {
if (
!(
id &&
(filter.fk_view_id || filter.fk_hook_id || filter.fk_parent_column_id)
)
) {
throw new Error(
`Mandatory fields missing in FILTER_EXP cache population : id(${id}), fk_view_id(${filter.fk_view_id}), fk_hook_id(${filter.fk_hook_id})`,
`Mandatory fields missing in FILTER_EXP cache population : id(${id}), fk_view_id(${filter.fk_view_id}), fk_hook_id(${filter.fk_hook_id}), fk_parent_column_id(${filter.fk_parent_column_id})`,
);
}
const key = `${CacheScope.FILTER_EXP}:${id}`;

42
packages/nocodb/src/schema/swagger.json

@ -15843,6 +15843,26 @@
]
}
},
"/api/v1/error-reporting": {
"post": {
"summary": "Error Reporting",
"operationId": "utils-error-report",
"responses": {
},
"description": "Error Reporting",
"tags": [
"Utils",
"Internal"
],
"requestBody": {
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorReportReq"
}
}
}
}
},
"/api/v1/db/meta/axiosRequestMake": {
"parameters": [
{
@ -27014,6 +27034,28 @@
"id": "psbv6c6y9qvbu"
}
},
"ErrorReportReq": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"stack": {
"type": "string"
}
}
}
},
"extra": {
"type": "object"
}
}
},
"Comment": {
"description": "Model for Comment",
"type": "object",

2
packages/nocodb/src/services/bases.service.ts

@ -166,7 +166,7 @@ export class BasesService {
if (
dataConfig?.client === 'pg' &&
process.env.NC_DISABLE_BASE_AS_PG_SCHEMA !== 'true'
process.env.NC_DISABLE_PG_DATA_REFLECTION !== 'true'
) {
baseBody.prefix = '';
baseBody.sources = [

20
packages/nocodb/src/services/utils.service.ts

@ -6,13 +6,14 @@ import { ViewTypes } from 'nocodb-sdk';
import { ConfigService } from '@nestjs/config';
import { useAgent } from 'request-filtering-agent';
import dayjs from 'dayjs';
import type { ErrorReportReqType } from 'nocodb-sdk';
import type { AppConfig, NcRequest } from '~/interface/config';
import { T } from '~/utils';
import { NC_APP_SETTINGS, NC_ATTACHMENT_FIELD_SIZE } from '~/constants';
import SqlMgrv2 from '~/db/sql-mgr/v2/SqlMgrv2';
import { NcError } from '~/helpers/catchError';
import { Base, Store, User } from '~/models';
import Noco from '~/Noco';
import { T } from '~/utils';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import getInstance from '~/utils/getInstance';
import { CacheScope, MetaTable, RootScopes } from '~/utils/globals';
@ -456,6 +457,10 @@ export class UtilsService {
ncMin: !!process.env.NC_MIN,
teleEnabled: process.env.NC_DISABLE_TELE !== 'true',
errorReportingEnabled: process.env.NC_DISABLE_ERR_REPORTS !== 'true',
sentryDSN:
process.env.NC_DISABLE_ERR_REPORTS !== 'true'
? process.env.NC_SENTRY_DSN
: null,
auditEnabled: process.env.NC_DISABLE_AUDIT !== 'true',
ncSiteUrl: (param.req as any).ncSiteUrl,
ee: Noco.isEE(),
@ -480,6 +485,19 @@ export class UtilsService {
return result;
}
async reportErrors(param: { body: ErrorReportReqType; req: NcRequest }) {
for (const error of param.body?.errors ?? []) {
T.emit('evt', {
evt_type: 'gui:error',
properties: {
message: error.message,
stack: error.stack?.split('\n').slice(0, 2).join('\n'),
...(param.body.extra || {}),
},
});
}
}
async feed(req: NcRequest) {
const {
type = 'all',

7
packages/nocodb/src/utils/encryptDecrypt.ts

@ -19,11 +19,14 @@ export const encryptPropIfRequired = ({
return;
}
const val =
typeof data[prop] === 'string' ? data[prop] : JSON.stringify(data[prop]);
if (!secret) {
return JSON.stringify(data[prop]);
return val;
}
return CryptoJS.AES.encrypt(JSON.stringify(data[prop]), secret).toString();
return CryptoJS.AES.encrypt(val, secret).toString();
};
export const decryptPropIfRequired = ({

21
packages/nocodb/src/utils/getInstance.ts

@ -39,10 +39,29 @@ export default async function (force = false, ncMeta = Noco.ncMeta) {
.count('storage as count')
.first()
.then((c) => c.count);
const tables = await ncMeta
.knex(MetaTable.MODELS)
.count('id as count')
.first()
.then((c) => c.count);
const views = await ncMeta
.knex(MetaTable.VIEWS)
.count('id as count')
.first()
.then((c) => c.count);
const nc_db_type = Noco.getConfig()?.meta?.db?.client;
res = { projectsMeta, projectsExt, impacted, nc_db_type, created, files };
res = {
projectsMeta,
projectsExt,
impacted,
nc_db_type,
created,
files,
tables,
views,
};
await NocoCache.set(CacheScope.INSTANCE_META, res);
}
return res;

4
packages/nocodb/src/version-upgrader/upgraders/0225002_ncDatasourceDecrypt.ts

@ -29,6 +29,8 @@ const decryptConfig = async (encryptedConfig: string, secret: string) => {
// decrypt datasource details in source table and integration table
export default async function ({ ncMeta }: NcUpgraderCtx) {
logger.log('Starting decryption of sources and integrations');
let encryptionKey = process.env.NC_AUTH_JWT_SECRET;
if (!encryptionKey) {
@ -75,6 +77,7 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
config: decrypted,
})
.where('id', source.id);
logger.log(`Decrypted source ${source.id}`);
passed.push(true);
} catch (e) {
logger.error(`Failed to decrypt source ${source.id}`);
@ -100,6 +103,7 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
config: decrypted,
})
.where('id', integration.id);
logger.log(`Decrypted integration ${integration.id}`);
passed.push(true);
} catch (e) {
logger.error(`Failed to decrypt integration ${integration.id}`);

2
tests/playwright/tests/db/views/viewForm.spec.ts

@ -1436,7 +1436,7 @@ test.describe('Form view: field validation', () => {
});
test.describe('Form view: conditional fields', () => {
if (enableQuickRun() || !isEE()) test.skip();
if (enableQuickRun()) test.skip();
let dashboard: DashboardPage;
let form: FormPage;

Loading…
Cancel
Save